2020-05-10 追記
中身が不定のJSONオブジェクトをGo言語で扱うのに mattn/go-jsonpointer が便利だった - えいのうにっき によると今なら mattn/go-jsonpointer が良さそうです。
元の文
まずgoでJSONを扱う基本はJSON and Go - The Go Programming Languageを参照してください。
mattyw: Using go to unmarshal json lists with multiple typesのブログ記事で紹介されていた内容が興味深かったので共有します。
このブログ記事は配列の要素に2つのデータタイプPerson, Placeが混在していて、それぞれJSONのキー名が異なる場合にどうデコードするかという話です。ブログのコメントにもあるように、APIの仕様を変えられるならキーを分けて配列の要素は混在しないようにしたほうがよいという話はあります。が、この例以外にもJSONの中身に応じてgo側では違う型にマッピングしたいケースはあると思いますので参考になります。
4つの実装が紹介されています。
- 一旦mapにデコードして、そこからstructのインスタンスを作る
- 2つのデータタイプのキーを両方持つstructを作ってそこにデコードしてから、最終的なstructのインスタンスを作る
- 一旦json.RawMessageにデコードして、そこからstructのインスタンスを作る。
- RawMessageは[]byteとして定義されているためこのキーを持っているかとかの判定は出来ません。そこでPersonとPlaceにそれぞれデコードしてみて、値が入った方を採用するということをしています。
-
dustin/go-jsonpointerというライブラリを使う。
- draft-ietf-appsawg-json-pointer-03 - JavaScript Object Notation (JSON) Pointerをgo言語で実装したものです。ざっくり言うとXMLに対するXPathのようなもので、JSON内の特定の位置を文字列で指定してその値を取得することができます。
- このライブラリを使ってデコードするサンプルコード
// tlehman's solution using dustin's go-jsonpointer
func solutionD(jsonStr []byte) ([]Person, []Place) {
persons := []Person{}
places := []Place{}
for i := int64(0); true; i++ {
// build path to element of json array (e.g. /things/2 )
pathRoot := string(strconv.AppendInt([]byte("/things/"), i, 10))
pathName := pathRoot + "/name"
pathAge := pathRoot + "/age"
pathCity := pathRoot + "/city"
pathCountry := pathRoot + "/country"
name, _ := jsonpointer.Find(jsonStr, pathName)
city, _ := jsonpointer.Find(jsonStr, pathCity)
if name != nil {
age, _ := jsonpointer.Find(jsonStr, pathAge)
agef, _ := strconv.ParseFloat(strings.TrimSpace(string(age)), 64)
nameTrimmed := string(trimJsonBytes(name))
persons = append(persons, Person{nameTrimmed, agef})
} else if city != nil {
country, _ := jsonpointer.Find(jsonStr, pathCountry)
cityTrimmed := string(trimJsonBytes(city))
countryTrimmed := string(trimJsonBytes(country))
places = append(places, Place{cityTrimmed, countryTrimmed})
} else {
break
}
}
return persons, places
}
go-jsonpointerの実装について
dustin/go-jsonpointerの実装は、goのJSON標準ライブラリのscanner.goに含まれる非公開API(先頭が小文字のscanner)を公開API(先頭が大文字のScanner)にしたdustin/gojsonに依存しています。
このScannerを使ってユーザにJSONポインタの文字列で指定されたキーまで進み、そこで必要な部分だけの値を取得できるという仕組みです。
標準のjson.Unmarshal()ではJSON全体に対応するgoのデータ構造を構築しますが、これだと関心がある部分についてのみデータ構造を作るので効率がいいというわけです。