GoでJSONをパースするときに、あれこれ考えるのだけど
きちんとやっていくと、どうしてもうまく実装できなくなって困っているという話。
先に書いておきますが、結論はないです。
JSONをパースするときの問題
そもそも、JSONを簡単に取り扱う必要がなぜあるのか、について。
標準では、JSONをパースする場合 json.Unmarshal
を使ったりjson.NewDecoder
を使ったりする。
例えば、User構造体に
type User struct {
Name string
Age uint16
}
func main() {
var jsonStr = `{
"Name": "Ken", "Age": 24
}`
var u User
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u) // {Ken 24}
}
上の場合、JSONの値にプロパティがないと何も値が入らない。
ところが、Goでは初期値として型の0値が入る。
なので、上のUser構造体の場合、Age
が設定されないままのため
値が0として取り扱われしまう。
これだと、本当に0歳なのか設定ミスなのかわからなくなってしまう。
func main() {
var jsonStr = `{
"Name": "Bob"
}`
var u User
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u) // {Ken 0}
}
これを解決するために、元となる構造体のフィールドをポインタ型にするとnilチェックができるようになる。
type User struct {
Name *string
Age *uint16
}
func main() {
var jsonStr = `{
"Name": "Ken"
}`
var u User
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u) // {0x2081ec2a0 <nil>}
}
ただこの場合、このJSONの結果がポインタ型だと意識して使わないといけなくなるので
使い勝手が悪くなる。なので、JSONを取り扱いやすいライブラリが欲しくなる。
よくあるJSONパッケージ
JSONを簡単に扱いたい場合に、以下のようなパッケージがある
jason
import "github.com/antonholmquist/jason"
func main() {
var jsonStr = `{
"Name": "Ken", "Age": 32
}`
var u User
var err error
v, err := jason.NewObjectFromBytes([]byte(jsonStr))
if err != nil {
panic(err)
}
u.Name, err = v.GetString("Name")
if err != nil {
panic(err)
}
var age float64
age, err = v.GetNumber("Age")
if err != nil {
panic(err) // ここで設定を検知する
}
u.Age = uint16(age)
fmt.Println(u) // {Ken, 32}
}
go-simplejson
import (
simpleJson "github.com/bitly/go-simplejson"
)
func main() {
var jsonStr = `{"Name": "Ken", "Age": 32}`
js, err := simpleJson.NewJson([]byte(jsonStr))
if err != nil {
panic(err)
}
var u User
u.Name, err = js.Get("Name").String()
if err != nil {
panic(err)
}
var age uint64
age, err = js.Get("Age").Uint64()
if err != nil {
panic(err) // ここで設定を検知する
}
u.Age = uint16(age)
fmt.Println(u) // {Ken, 32}
}
go-scan
import (
scan "github.com/mattn/go-scan"
)
func main() {
var jsonStr = `{"Name": "Ken", "Age": 32}`
var u User
r := strings.NewReader(jsonStr)
if err := scan.ScanJSON(r, "/Name", &u.Name); err != nil {
panic(err)
}
r = strings.NewReader(jsonStr)
if err := scan.ScanJSON(r, "/Age", &u.Age); err != nil {
panic(err)
}
fmt.Println(u) // {Ken, 32}
}
ちなみに、ScanTree()
のほうが使いやすいと思う。
func main() {
var jsonStr = `{"Name": "Ken", "Age": 32}`
var u User
var i interface{}
if err := json.Unmarshal([]byte(jsonStr), &i); err != nil {
panic(err)
}
if err := scan.ScanTree(i, "/Name", &u.Name); err != nil {
panic(err)
}
if err := scan.ScanTree(i, "/Age", &u.Age); err != nil {
panic(err)
}
fmt.Println(u) // {Ken, 32}
}
ベンチマーク
上3つのライブラリのベンチマークの結果はこんな感じ。
% go test -bench .
testing: warning: no tests to run
PASS
BenchmarkJason 100000 20752 ns/op
BenchmarkSimpleJSON 200000 9710 ns/op
BenchmarkGoScan 100000 18262 ns/op
型にUnmarshalを実装する
型にjson.Unmarshalerインタフェースを実装することで、
JSONをデコードしたときに、そちらを呼び出すことができる。
package main
import (
"encoding/json"
"fmt"
scan "github.com/mattn/go-scan"
)
type User struct {
Name string
Age uint16
}
func (u *User) UnmarshalJSON(data []byte) error {
var i interface{}
if err := json.Unmarshal(data, &i); err != nil {
return err
}
if err := scan.ScanTree(i, "/Name", &u.Name); err != nil {
return err
}
if err := scan.ScanTree(i, "/Age", &u.Age); err != nil {
return err
}
return nil
}
func main() {
var jsonStr = `{"Name": "Ken", "Age": 32}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
if err != nil {
panic(err)
}
fmt.Printf("Name: %s Age: %d\n", u.Name, u.Age) // name: Ken Age: 32
}
ネストされたJSONの場合
ここから上手いやり方がわからない話。
たとえば、下のようなネストされたデータを持ちたい場合。
そして Article.Title
と Article.Content
を必須にしたい場合。
{
"Name": "Ken",
"Age": 32,
"Articles": [
{"Title": "Title A", "Content": "content"},
{"Title": "Title B", "Content": "content"}
]
}
構造体もArticle
を入れる。
type User struct {
Name string
Age uint16
Articles []Article
}
type Article struct {
Title string
Content string
}
前の段落で書いたUnmarshalerインターフェースを実装しなければ、
構造体への展開はよろしくやってくれる。
func main() {
var jsonStr = `{
"Name": "Ken",
"Age": 32,
"Articles": [
{"Title": "Title A", "Content": "content"},
{"Title": "Title B", "Content": "content"}
]
}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
if err != nil {
panic(err)
}
fmt.Println(u) // {Ken 32 [{Title A content} {Title B content}]}
}
ところが、上のUnmarshalerインターフェースを実装すると、これができなくなる。
なので、func (u *User) UnmarshalJSON(data []byte) error
側に直接記載する必要が出てくる。
func (u *User) UnmarshalJSON(data []byte) error {
js, err := simpleJson.NewJson(data)
if err != nil {
return err
}
u.Name, err = js.Get("Name").String()
if err != nil {
return err
}
var age uint64
age, err = js.Get("Age").Uint64()
if err != nil {
return err
}
u.Age = uint16(age)
// Article
var bytes []byte
bytes, err = js.Get("Articles").Encode()
if err != nil {
return err
}
var as []Article
if err := json.Unmarshal(bytes, &as); err != nil {
return err
}
u.Articles = as
return nil
}
Article
もUser
のようにUnmarshalJSON
を実装する
func (a *Article) UnmarshalJSON(data []byte) error {
js, err := simpleJson.NewJson(data)
if err != nil {
return err
}
a.Title, err = js.Get("Title").String()
if err != nil {
return err
}
a.Content, err = js.Get("Content").String()
if err != nil {
return err
}
return nil
}
バリデーションとパースを分ける
そうなるともうバリデーションとパースを完全に分けたほうがいいんじゃなかろうかってなる。
なので、json-schemaを用意して、gojsonschemaを使って最初にバリデーションをかけてみる
func main() {
var jsonStr = `{
"Name": "Ken",
"Age": 32,
"Articles": [
{"Title": "Title A", "Content": "content A"},
{"Title": "Title B", "Content": "content B"}
]
}`
schema := `{
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Age": {
"type": "integer"
},
"Articles": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Title": {
"type": "string"
},
"Content": {
"type": "string"
}
},
"additionalProperties": false,
"required": ["Title", "Content"]
}
}
},
"additionalProperties": false,
"required": ["Name", "Age", "Articles"]
}`
var j map[string]interface{}
err := json.Unmarshal([]byte(schema), &j)
if err != nil {
panic(err)
}
schemaDocument, err := gojsonschema.NewJsonSchemaDocument(j)
if err != nil {
panic(err)
}
var jsonBody interface{}
err = json.Unmarshal([]byte(jsonStr), &jsonBody)
if err != nil {
panic(err)
}
result := schemaDocument.Validate(jsonBody)
if result.Valid() == false {
panic("err")
}
var u User
err = json.Unmarshal([]byte(jsonStr), &u)
if err != nil {
panic(err)
}
fmt.Println(u) // {Ken 32 [{Title A content} {Title B content}]}
}
これで、Articles
の中身もjson-schemaで指定した通りにチェックしてくれる。
まとめ
いろいろとグダグダ書いてみたけれど
まとめるとこんな感じ。
- json.Unmarshalで簡単にJSONをパースできる
- ゼロ値が意図的に設定されたものなのかを確認したい場合
- ポインタ型で受け取って
nil
チェックをする- ポインタ把握が若干ツラい
-
map[string]interface{}
で受け取ってチェックをする (上では記載してない)- 型キャストがたくさんできてツラい
- ライブラリを使って値をチェックする
- 処理を書く場所は?
- 毎回Unmarshalする前に書く
- 値を入れたい構造体Unmarshalerインタフェースを実装する
- ポインタ型で受け取って
- ネストされたJSONをパースしたい場合
- 構造体をネスト構造にしてjson.Unmarshal
- ゼロ値のチェックが必要な場合は?
- Unmarshalerインターフェースを実装して、そこに手動で書く
- 組み合わせがあったときにつらい
- バリデーションとパースを分けて考える
- バリデーション
-
jason
go-simplejson
go-scan
とか - json-schemaを用意してgojsonschemaを使って、パースをする
-
- バリデーション
状況に応じてバリエーションがあってなかなか
コレだという感じがない。
なんかもっと良い方法があるのではないかとモヤモヤ思う今日この頃です。