Goのjson.Unmarshalについてのメモ

jsonを文字列をgoの構造体にするときにお世話になるjson.Unmaarshalを使い、json文字列ではネストした構造のデータを基本型だけをフィールドとしてもつ構造体にunmarshalする方法についてのメモ。

結論

先に結論を書いておくと、以下の例のように変換先の構造体でUnmarshalメソッドを実装すればいいです。

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    FirstName string `json:"firstname"`
    LastName  string `json:"lastname"`
    Age       int64  `json:"age"`
}

func (s *Person) UnmarshalJSON(data []byte) error {
    type Name struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
    }

    type person struct {
        Name Name  `json:"name"`
        Age  int64 `json:"age"`
    }

    var tmp person
    if err := json.Unmarshal(data, &tmp); err != nil {
        log.Fatal(err)
    }

    *s = Person{
        FirstName: tmp.Name.FirstName,
        LastName:  tmp.Name.LastName,
        Age:       tmp.Age,
    }
    return nil
}

func main() {
    data := []byte(`{
  "name": {
      "firstname" : "first",
      "lastname" : "last"
  },
  "age": 20
}`)
    var p Person
    json.Unmarshal(data, &p)
    bytes, _ := json.Marshal(p)
    fmt.Printf("%s", bytes)

}

https://play.golang.org/p/m5dH2gp8OGU

実行結果はこちら。

{"firstname":"first","lastname":"last","age":20}

Unmarshalの仕組み

Unmarshal関数でJSONをGoの構造体に変換するときは、以下のようなルールで変換が行われます。
詳しいルールはドキュメントを参照してください。
https://golang.org/pkg/encoding/json/#Unmarshal

  • Unmarshalに引数として渡した構造体のフィールド名またはjsonタグがJSONのキー名と一致した場合に、フィールドの値としてJSONの値を格納する。
  • 構造体のフィールド名またはjsonタグに一致しないフィールドは無視される。

シンプルなJSONをunmarshalする

ネストがないシンプルなJSONをUnmarshlするのはとっても簡単。
JSONのキー名に対応するフィールド名を持つ構造体を定義してあげて、構造体のポインターjson.Unmarshalに渡して上げればいいです。

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age  int64
}

func main() {
    data := []byte(`{
      "name" : "first last",
      "age" : 20
  }`)

    var person *Person
    if err := json.Unmarshal(data, &person); err != nil {
        panic(err)
    }
    fmt.Printf("%+v", person)
}

実行結果

&{Name:first last Age:20}

すごく簡単!!

ネストがあるJSONをunmarshlする

JSONの中にネストがある場合には少しだけ複雑で、jsonタグやフィールド名ではネストが表現できないので、
ネストの中身に対応した構造体を定義して(下記の例だとName型)、その型をフィールドとしてもつ構造体のポインターjson.unmarshalに渡す必要があります。

import (
    "encoding/json"
    "fmt"
)

type (
    Name struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
    }

    Person struct {
        Name `json:"name"`
        Age  int64 `json:"age"`
    }
)

func main() {
    data := []byte(`{
  "name": {
      "firstname" : "first",
      "lastname" : "last"
  },
  "age": 27
}`)
    var p Person
    json.Unmarshal(data, &p)
    bytes, _ := json.Marshal(p)
    fmt.Printf("%s", bytes)

}

実行結果

{"name":{"firstname":"first","lastname":"last"},"age":27}

ネストされたデータがあるJSONをネストがないようにunmarshalする(flatten)

unmarshalを行うだけなら上記の方法で特に困ることはないのですが、unmarshalした後の処理でデータにネストがあると扱いづらい時もあると思います。
要はこれを

{
    "name": {
        "firstname" : "first",
        "lastname" : "last"
    },
    "age": 27
}

この構造体にしたい。

type Person struct {
    FirstName string `json:"firstname"`
    LastName  string `json:"lastname"`
    Age       int64  `json:"age"`
}

JSONの中にネストがある場合には、ネストしたデータに対応する構造体を定義する必要があるので、先程の例のような変換を行ったあとに、基本型だけをフィールドとして持つ構造体にマッピングさせます。

import (
    "encoding/json"
    "fmt"
)

type (
    Name struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
    }

    Person struct {
        Name `json:"name"`
        Age  int64 `json:"age"`
    }

    FlatPerson struct {
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
        Age       int64  `json:"age"`
    }
)

func main() {
    data := []byte(`{
    "name": {
        "firstname" : "first",
        "lastname" : "last"
    },
    "age": 27
}`)
    var p Person
    json.Unmarshal(data, &p)
    // unmarshal後に別の構造体にマッピングしている
    f := FlatPerson{
        FirstName: p.FirstName,
        LastName:  p.LastName,
        Age:       p.Age,
    }
    bytes, _ := json.Marshal(f)
    fmt.Printf("%s", bytes)
}

実行結果

{"firstname":"first","lastname":"last","age":27}

やりたかったことはできたが、unmarshal後に毎回マッピングするのは面倒、unmarshal時にやってしまいたいです。
json.Unmarshal関数は第二引数として渡された型がUnmarshalerインターフェイスを実装している場合には、その型のUnmarshalJSONを呼び出します。
UnmarshalerインターフェイスUnmarshalJSONだけを持つシンプルなインターフェイスなので、json.Unmarshalの第二引数として渡したい型にUnmarshalJSONを実装してあげるだけでいいです。

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

ということで、第二引数に渡している型にUnmarshalJSONを実装したものが、一番最初に結論として記載しているコードになります。

参考にさせて頂いたサイト