Goのjson.Marshal/Unmarshalの仕様を整理してみる

August 13, 2014 - golang

TL;DR

  • goの構造体につけるタグは、フォーマットが不正だと読み込まれない(当然)
  • json.Marshalは、構造体のjsonタグがあればその値をキーとしてJSON文字列を生成する
  • json.Unmarshalは、構造体のjsonタグがあればその値を対応するフィールドにマッピングする
    • jsonタグがなければ、完全一致もしくはcase-insensitiveなフィールドにマッピングする

では、ひとつづつ確認していきます。

goの構造体につけるタグは、フォーマットが不正だと読み込まれない(当然)

goの構造体にはタグの機能があって、型の後に特定のフォーマットでアノテーションが記述できます

type MyType struct {
  Hoge string `foo:"1" bar:"2"`
}

key:"value"をスペース区切りで複数かけます。 で、アクセスするにはリフレクションを使います。

t := reflect.TypeOf(MyType{})
tagFoo := t.Field(0).Get("foo")
tagBar := t.Field(0).Get("bar")
fmt.Printf("foo=%s, bar=%s\n", tagFoo, tagBar)
// foo=1, bar=2

タグのフォーマットが不正な場合は、値が空になります。コンパイルエラーにはなりません。

type MyType struct {
  Hoge string `foo:1`
}
t := reflect.TypeOf(MyType{})
tagFoo := t.Field(0).Get("foo")
fmt.Printf("foo=%#v\n", tagFoo)
// foo=""

このエラーをコンパイルする前に検知するには、go vetコマンドが使えます。

$ go vet my_type.go
my_type.go:10: struct field tag `foo:1` not compatible with reflect.StructTag.Get

できればgo compile実行したときに自動的にチェックしてほしいところですが…

で、jsonパッケージのMarshal/Unmarshalは、このタグを使ってアノテーションを記述することができます。

json.Marshalは、構造体のjsonタグがあればその値をキーとしてJSON文字列を生成する

json.Marshalは構造体からJSON文字列への変換する関数です。

type MyType struct {
  A string
  FooBar string
}

mt := MyType{"aaaa", "baz"}
b, _ := json.Marshal(mt)
fmt.Printf("%s\n", string(b))
// {"A":"aaaa","FooBar":"baz"}

普通にやると、フィールド名がそのままJSONのキーになります。

ここで構造体のタグ機能をつかって、フィールド名のアノテーションを書くことで、任意のフィールド名でJSONが生成できます。

type MyType struct {
  A string `json:"a"`
  FooBar string `json:"foo_bar"`
}

mt := MyType{"aaaa", "baz"}
b, _ := json.Marshal(mt)
fmt.Printf("%s\n", string(b))
// {"a":"aaaa","foo_bar":"baz"}

アノテーション指定した”a”がJSONのキーとして使用されていることが確認できます。

json.Unmarshalは、構造体のjsonタグがあればその値を対応するフィールドにマッピングする

json.UnmarshalはJSON文字列から構造体へ変換する関数です。

まずはタグなしのパターン

type MyType struct {
  A string
  FooBar string
}

var mt MyType
json.Unarshal([]byte(`{"A":"aaa", "FooBar":"baz"}`, &mt)
fmt.Printf("%#v\n", mt) // main.MyType{A:"aaaa",FooBar:"baz"}

JSONのフィールド名がsnake_caseのパターン

var mt MyType
json.Unarshal([]byte(`{"a":"aaa","foo_bar":"baz"}`, &mt)
fmt.Printf("%#v\n", mt) // main.MyType{A:"aaaa", FooBar:""}

ここで「ん?」となるわけです。タグによるアノテーションがないのになぜ小文字のフィールドがちゃんと読み込まれているのか…と。

この仕様、json.Unmarshalのgodocにちゃんと書いてあります。

func Unmarshal

To unmarshal JSON into a struct, Unmarshal matches incoming object keys to the keys used by Marshal (either the struct field name or its tag), preferring an exact match but also accepting a case-insensitive match.

どうやら、json.Unmarshalは、JSONに含まれるキーと完全一致か、case-insensitiveに一致する構造体のフィールドにマッピングする仕様となっているようです。もちろん、snake_caseなJSONのキーはアノテーションがないとマッピングされず無視されます。(構造体のフィールドとしてsnale_caseが存在しない限りは)

経緯

先日、ヒカルのGO! hikarie.go #2@yosuke_furukawaさんによるGoでJSON APIを書いてみるというハンズオンを体験してきました。

そこで、アノテーションをつけた構造体を使ってMarshalした時に、書いたコードがこれです。

type User struct {
  Name string `json:name`
}
user := User{"restartr"}
b, _ := json.Marshal(user)
fmt.Printf("%s\n", string(b)) // {"Name":"restartr"}

小文字で出力してほしいのに、アノテーションが効かない…となったわけです。 他の参加者がjson:"name"でないといけないことに気づいて、この件は一件落着。

だったのですが、もう少し詳しく調べると上に整理したような仕様が見えてきたというわけです。

type User struct {
  Name string `json:name` // 実際は無視されている
}
var user User
json.Unmarshal([]byte(`{"name":"restartr"}`, &user)
fmt.Printf("%s\n", user) // main.User{Name:"restartr"}

実はこの時書いたjson:nameというアノテーションは不正なフォーマットとして無視されていて、たまたまUser.Nameというフィールドにcase-insensitiveでUnmarshalした時にマッピングができていたにすぎなかったようです。

とまぁそんな感じでJSONの取り扱いについて正しい知識を得られたのですが、一人でやってた時には気づかなかった疑問とか知識として不足している点に気付けるので、ハンズオン形式も良いものですね。