Edited at
GoDay 8

Goのjson.Decoderの使い方

More than 3 years have passed since last update.

この記事は Go Advent Calendar 2015 の8日目の記事です。


はじめに

GoでJSONを使用したい時、json.Unmarshalを使用すると簡単に変換ができます。

しかし、ユーザデータのインポート処理などで、例えば配列の要素が100万個あるような大きめのデータの場合、そのまま全体をjson.Unmarshalするのではなく、ストリームとして扱いたいです。

そんな時に使えるのが、json.Decoderになります。


基本的な使い方

データ構造が、JSONオブジェクトの羅列の場合にはシンプルに使えます。

DecoderのExampleを引用しますが、

func main() {

const jsonStream = `
{"Name": "Ed", "Text": "Knock knock."}
{"Name": "Sam", "Text": "Who's there?"}
{"Name": "Ed", "Text": "Go fmt."}
{"Name": "Sam", "Text": "Go fmt who?"}
{"Name": "Ed", "Text": "Go fmt yourself!"}
`
type Message struct {
Name, Text string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))
for {
var m Message
if err := dec.Decode(&m); err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", m.Name, m.Text)
}
}

このようになります。

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

json.NewDecoderでjsonStremを読み取るDecoderを生成し、Decodeメソッドを呼ぶ際に値を設定したいstructのポインタを渡すだけです。

これ以上読み取れなくなると、nil, io.EOFが返ってきます。他には、JSONのフォーマットになっていない場合にもエラーが返ってきます。

dec.Decodeの処理はjson.Unmarshalと同じく、structの定義の際にタグを記述することで細やかな変換ができます。

jsonStreamは1行1JSONオブジェクトになっていますが、1行1JSONオブジェクトである必要はありません。{から}までを読み取って変換してくれるようです。↓このようなデータでも問題なく読み取りができます。

    const jsonStream = `

{"Name": "Ed",
"Text": "Knock knock."}
{"Name": "Sam", "Text": "Who's there?"}{"Name": "Ed", "Text": "Go fmt."}
{
"Name": "Sam", "Text": "Go fmt who?"
}
{"Name": "Ed", "Text": "Go fmt yourself!"}
`


配列の中にオブジェクトがある場合

こちらもサンプルにあるとおりですが、

func main() {

const jsonStream = `
[
{"Name": "Ed", "Text": "Knock knock."},
{"Name": "Sam", "Text": "Who's there?"},
{"Name": "Ed", "Text": "Go fmt."},
{"Name": "Sam", "Text": "Go fmt who?"},
{"Name": "Ed", "Text": "Go fmt yourself!"}
]
`
type Message struct {
Name, Text string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))

// read open bracket
t, err := dec.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%T: %v\n", t, t)

var m Message
// while the array contains values
for dec.More() {

// decode an array value (Message)
err := dec.Decode(&m)
if err != nil {
log.Fatal(err)
}

fmt.Printf("%v: %v\n", m.Name, m.Text)
}

// read closing bracket
t, err = dec.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%T: %v\n", t, t)

}

このようになります。

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

最初のサンプルと異なり、dec.Token(),dec.More()というのが登場しています。

簡単に説明すると、TokenはJSONのトークンを取得するためのメソッドです。トークンとは、Delimiter( []{} )やオブジェクトのキー、バリューのことで、コロンやコンマは省略されます。要素がなくなると、nil, io.EOFが返ってきます。

Moreは、配列やオブジェクトの次の要素があるとtrueが取得できます。

つまりこの例では、jsonStreamがオブジェクトの配列になっているので、1度dec.TokenでDelimiterを取得して、forとdec.Moreを使用して配列の要素にアクセスするという形の処理になっています。


オブジェクトのプロパティの一部をデコードしたい

例えばユーザ一覧にインポート先のグループIDが付属している、というパターンです。私が使用したかったのもこれに近いもので、usersの要素数が大きくなる想定です。

{

"group_id": 1,
"users": [
{ "name": "user1", "email": "user1@example.com" },
{ "name": "user2", "email": "user2@example.com" },
{ "name": "user3", "email": "user3@example.com" },
{ "name": "user4", "email": "user4@example.com" }
]
}

このデータでは読み取りたいものは、キーがusersの配列の値だけですので、以下のような処理になります。

func main() {

const jsonStream = `
{
"group_id": 1,
"users": [
{ "name": "user1", "email": "user1@example.com" },
{ "name": "user2", "email": "user2@example.com" },
{ "name": "user3", "email": "user3@example.com" },
{ "name": "user4", "email": "user4@example.com" }
]
}
`
type User struct {
Name, Email string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))

// users まで進める
for {
t, err := dec.Token()
if err != nil {
log.Fatal(err)
}
if t == "users" {
break
}
}

// [ を読み取る
_, err := dec.Token()
if err != nil {
log.Fatal(err)
}
// 配列内部のDecode
for dec.More() {

var u User
err := dec.Decode(&u)
if err != nil {
log.Fatal(err)
}

fmt.Printf("%v: %v\n", u.Name, u.Email)
}
}

https://play.golang.org/p/08NlriBBbB

欲しいデータはusersの中だけなので、usersが見つかるまで読み飛ばします。それから、[ があるので、それを読み込み、配列の内部をforループで読み取ってデコードしていきます。

dec.Token()で取得したTokenが何かをチェックしながら処理を進めれば、もっと複雑な構造でもなんとかなりそうな気がします。


カンマが多いときのエラー

ちょっとハマってしまったので紹介しておきます。

Decoderはご覧になったとおり、先頭から順次読み込んでいくので、もともとJSONのフォーマットとしておかしい場合には、読み取りができなくなったところでエラーが返ってきます。

例えば次のような配列の要素の最後にカンマがついている、フォーマットが正しくないデータの場合、

{

"group_id": 1,
"users": [
{ "name": "user1", "email": "user1@example.com" },
{ "name": "user2", "email": "user2@example.com" },
{ "name": "user3", "email": "user3@example.com" },
{ "name": "user4", "email": "user4@example.com" },
]
}

4行目のuser4を読み取ったあと、5回目のDecodeで

invalid character ']' looking for beginning of value

というエラーが返ってきます。

Decoderとしてはカンマがあったので次のデータが来るつもりが、] が来たのでエラーになるという状況です。全体のフォーマットチェックなどは行わないので、その辺り注意してください。

以上、json.Decoderの使い方でした。