Go で JSON を取り扱う際のプラクティス集

これは [Go2 Advent Calender 2020](https://qiita.com/advent-calendar/2020/go2) の 17 日目の記事です。 

Go は `encoding/json` という JSON を取り扱うための公式 package を提供しており、 
簡単に JSON を取り扱うことができます。 
今回は、その package を使って JSON を取り扱う際の代表的なプラクティスをまとめました。 

## ターゲットとする読者

本記事がターゲットとする読者は以下の通りです。 

* 今から Go で JSON をこねこねしようと思っている人
* Go で JSON を扱ったことがあるけど、プラクティス集があると振り返れていいなと思う人

## Practice 1: 構造体との Unmarshal/Marshal

以下は一番基本の JSON の decode/encode のスタイルです。 
JSON に対応した構造体を定義し、その構造体へ JSON を decode/encode します。 
API 名としては、decode = Unmarshal、encode = Marshal として表現されています。 

### Unmarshal

```go
package main

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

func main() {
	type Dog struct {
		DogBreed string
		Name     string
		Age      string
	}
	jsonBlob := []byte(`{"DogBreed": "Shiba", "Name": "Taro", "Age": "1"}`)

	var dog Dog

	err := json.Unmarshal(jsonBlob, &dog)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%v\n", dog)
}
```

```
$ go build -o a.exe && a.exe
{Siba Taro 1}
```

### Marshal

```go
package main

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

func main() {
	type Dog struct {
		DogBreed string
		Name     string
		Age      string
	}
	dog := Dog{DogBreed: "Shiba", Name: "Taro", Age: "1"}

	var jsonBlob []byte

	jsonBlob, err := json.Marshal(&dog)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%v\n", string(jsonBlob))
}
```

```
$ go build -o a.exe && a.exe
{"DogBreed":"Shiba","Name":"Taro","Age":"1"}
```

### tips

* JSON の key と構造体のメンバ名は一致している必要がある
    * JSON の key と異なる名前を付ける場合は、JSON tag を付与する (この記事では説明していない)
* 構造体のメンバは export (CamelCase に) する必要がある

## Practice 2: Unmarshal 方法のバリエーション

Practice 1 の方法は定番の方法ではあるものの、構造体を定義したりやや面倒な方法ではあります。 
Unmarshal/Marshal は `*interface{}` を渡せる仕様となっているため、以下の様に記述することができます。 

```go
package main

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

func main() {
	jsonBlob := []byte(`{"DogBreed": "Shiba", "Name": "Taro", "Age": "1"}`)

	var dog interface{}

	err := json.Unmarshal(jsonBlob, &dog)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%v\n", dog)
}
```

```
$ go build -o a.exe && a.exe
map[Age:1 DogBreed:Shiba Name:Taro]
```

### tips

* `*interface{}` を渡した場合、`map` 型で Unmarshal 結果が返ってくる

## Practice 3: 遅延デコード

Practice1、2 のコードでは、一回の encode/decode 処理で JSON を Go のデータオブジェクトへ変換していました。 
しかし、それでは「ある JSON の key の値に従って、JSON の decode の仕方を変更したい」場合に上手く decode することができません。 
それに対応する方法としては、以下のコードの様に、遅延デコードしたい JSON key を `*json.RawMessage` へ一旦保存しておく方法があります。 

```go
package main

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

func main() {
	type Data struct {
		Type   string
		Person json.RawMessage
	}
	type PersonA struct {
		Name string
		Age  string
	}
	type PersonB struct {
		Name     string
		Birthday string
	}
	jsonStr := `[{"Type":"A", "Person":{"Name":"taro","Age":"1"}},{"Type":"B", "Person":{"Name":"Jiro","Birthday":"2020/11/15"}}]`
	var data []Data
	if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
		log.Fatal(err)
	}
	for _, d := range data {
		switch d.Type {
		case "A":
			var p PersonA
			if err := json.Unmarshal(d.Person, &p); err != nil {
				log.Fatal(err)
			}
			fmt.Printf("%+v\n", p)
		case "B":
			var p PersonB
			if err := json.Unmarshal(d.Person, &p); err != nil {
				log.Fatal(err)
			}
			fmt.Printf("%+v\n", p)
		default:
			log.Fatal("invalid type")
		}
	}
}
```

```
$ go build -o a.exe && a.exe
{Name:taro Age:1}
{Name:Jiro Birthday:2020/11/15}
```

### tips

* `json.RawMessage` 型の実体は `[]byte` のため、その JSON byte 列に対して再度 Unmarshal をするイメージ

## まとめ

* encoding/json で大体のやりたいことは実現できる
* loose に decode する方法についても提供されている
* 逆に strict な key の存在チェックなどについては実装されていないため、やりたい時はサードパーティ等に頼る必要がある

## References

* [encoding/json](https://golang.org/pkg/encoding/json/#example_Unmarshal)
* [golang は ゆるふわに JSON を扱えまぁす!
](https://www.kaoriya.net/blog/2016/06/25/)
* [Introduction to JSON handling in Go
](https://go-talks.appspot.com/github.com/d-tsuji/talk/20201114_umeda_go/main.slide#9)