Go

GoでJSONをパースするときに考えることになるアレこれ

More than 3 years have passed since last update.

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

https://github.com/antonholmquist/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

https://github.com/bitly/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

https://github.com/mattn/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
}

https://gist.github.com/hiroosak/af65464863cda7dc494a


ネストされたJSONの場合

ここから上手いやり方がわからない話。

たとえば、下のようなネストされたデータを持ちたい場合。

そして Article.TitleArticle.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
}

ArticleUserのように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
}

https://gist.github.com/hiroosak/2cc2ab4a8da6cc7d78c4


バリデーションとパースを分ける

そうなるともうバリデーションとパースを完全に分けたほうがいいんじゃなかろうかってなる。

なので、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}]}
}

https://gist.github.com/hiroosak/c70066ed92b0c847f499

これで、Articlesの中身もjson-schemaで指定した通りにチェックしてくれる。


まとめ

いろいろとグダグダ書いてみたけれど

まとめるとこんな感じ。



  • json.Unmarshalで簡単にJSONをパースできる

  • ゼロ値が意図的に設定されたものなのかを確認したい場合


    • ポインタ型で受け取ってnilチェックをする


      • ポインタ把握が若干ツラい




    • map[string]interface{}で受け取ってチェックをする (上では記載してない)


      • 型キャストがたくさんできてツラい



    • ライブラリを使って値をチェックする



    • 処理を書く場所は?


      • 毎回Unmarshalする前に書く

      • 値を入れたい構造体Unmarshalerインタフェースを実装する





  • ネストされたJSONをパースしたい場合


    • 構造体をネスト構造にしてjson.Unmarshal

    • ゼロ値のチェックが必要な場合は?


      • Unmarshalerインターフェースを実装して、そこに手動で書く

      • 組み合わせがあったときにつらい



    • バリデーションとパースを分けて考える


      • バリデーション



        • jason go-simplejson go-scanとか

        • json-schemaを用意してgojsonschemaを使って、パースをする







状況に応じてバリエーションがあってなかなか

コレだという感じがない。

なんかもっと良い方法があるのではないかとモヤモヤ思う今日この頃です。