0
0

More than 1 year has passed since last update.

Go言語におけるJSONのDecodeの注意点・対策

Last updated at Posted at 2022-03-30

ここ数ヶ月、APIのレスポンスをParseする処理をGo言語で書く機会がたくさんありました。
APIから返ってくるJSONの形式によっては、json.Unmarshal ではどうにも太刀打ちできないことが何度かありました。

Go言語でJSONをParseする際に注意すべきこと、使える手段についてまとめておきます

何はともあれコレ! json.Unmarshal

  • func Unmarshal(data []byte, v interface{}) error
  • 標準ライブラリ json のメソッド。
    • json形式のbyte列を受け取り、Parseして v が指す値に格納する。第二引数 v はpointerでなければならない。
サンプルコード
import (
	"encoding/json"
	"fmt"
)

func main() {
	var jsonBlob = []byte(`[
	{"Name": "Platypus", "Order": "Monotremata"},
	{"Name": "Quoll",    "Order": "Dasyuromorphia"}
]`)
	type Animal struct {
		Name  string
		Order string
	}
	var animals []Animal
	err := json.Unmarshal(jsonBlob, &animals) // <- 第二引数はポインタ。メソッド内で代入は完了する
	if err != nil {
		fmt.Println("error:", err)
	}
	fmt.Printf("%+v", animals) // => [{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]
}
  • 関数の名前について

    • コンピュータサイエンスにおいて、marshallingとは、「送信に適したデータ形式(この場合はjson)に変換する」という意味を持つ。
      • シリアライゼーション(serialization)とほぼ同義。
    • この逆操作(シリアライズされたオブジェクトを内部データ構造に変換すること)をUnmarshallingと呼ぶ。
  • マッピングルール

    • jsonUnmarshalは、渡されたjsonのKey名と一致するStructのFieldに値を入れる。この時、大文字・小文字が一致していなくても構わない。

参考

ケース1: ケバブケースのフィールド名

問題

Ubiregi APIというAPIのクローラーを開発している中で、こんなレスポンスに出会いました。

ubiregiのresponse
{
  "timestamp": "2021-09-12T13:40:15Z",
  "next-url": "https://ubiregi.com/api/3/xxxx"
}

next-url というフィールド名は曲者です。Go言語ではStructのフィールド名に-(ハイフン)を使えません。これはどうやってマッピングしたらいいでしょうか?

回答:構造体タグを用いたマッピングのカスタマイズ

こんな時、json.Unmarshal構造体タグによって動作をカスタマイズできます。

  • 構造体タグとは
    • 構造体(Struct)にメタ情報を付与できる記法
    • 型情報を扱う標準ライブラリreflectの機能を使うことで取得できる
構造体タグの例
type Organization struct {
    Name     string `json:"name" validate:"required"`
    Language string `json:"language,omitempty" validate:"required"`
    CreatedAt time.Time `json:"-"`
}

さて、上記のUbiregi APIのレスポンスをParseする際は、Structを以下のように指定すればOKです。

type CheckoutsResponse struct {
	Timestamp time.Time `json:"timestamp"`
	NextURL   string    `json:"next-url"`
}

あとは普通にUnmarshalするだけ。

var output CheckoutsResponse
err := json.Unmarshal(responseBody, &output)

参考記事

ケース2: RFC3339以外のフォーマットの時刻情報をParseする

問題

Twitter Premium API v1.1 Search APIというAPIのクローラーを開発している中で、こんなレスポンスに出会いました。

{
	"id": 123456789,
	"created_at": "Thu Oct 07 12:14:13 +0000 2021",
	...
}

json.Unmarshalは、RFC3339(2006-01-02T15:04:05Z07:00)の形式であれば問題なくParseできますが、それ以外の形式のデータはそのままではフォーマットできません。
この created_at は RubyDate(Mon Jan 02 15:04:05 -0700 2006)という形式なので、一手間必要です。どうすればいいでしょうか?

(時刻のフォーマットって何?という方は、Go 標準ライブラリ timeの記事を読んでください)

回答

  • 独自型を作成し、UnmarshalJSON というメソッドを定義することで解消できます。
  • Go で時刻を json.Unmarshal する際の注意点の解説がわかりやすいので、こちらに内容を譲ります。
  • 他にも DecodeHook を使う手もありますが、ここでは省略します。

ケース3: データ形式が変わる

問題

同じく、Twitter Premium API v1.1 Search APIというAPIのクローラーを開発している中でGeoデータ(地理データ)をParseする機会がありました。

geoデータは Point(=点) と Polygon(=面積)の2パターンがあり、それぞれ形式が異なります。

point
"geo": {
    "coordinates": [
        135.75385,
        35.02107
    ],
    "type": "Point"
}
Polygon
"geo": {
    "coordinates": [
        [
            [
                -74.026675,
                40.683935
            ],
            [
                -74.026675,
                40.877483
            ],
            [
                -73.910408,
                40.877483
            ],
            [
                -73.910408,
                40.3935
            ]
        ]
    ],
    "type": "Polygon"
}

Pointの場合、coordinates[2]float64 になりますが、Polygonの場合は [][][2]float64となります。
つまり、geoのデータ形式は、ツイートによって異なるということです。
この場合、どのように書いたらいいのでしょうか。

回答

このようなケースには、mapstructure というライブラリを使うと対応できます。

mapstructureは、まずbyte列をmap[string]interface{}に変換し、次にmap[string]interface{}を指定のStructに変換する、という二段階の変換を必要とします。なぜかについては、公式ドキュメントのBut Why?の部分を読んでください。

細かい使い方はmapstructureを使って複雑な構造のJSONを構造体にマッピングするがわかりやすいのでこちらを参照のこと。

まとめ

  • Go言語でjsonのDecodeを行うときは、通常は標準ライブラリの json.Unmarshal メソッドを利用する
  • json.Unmarshal は、渡されたjsonのKey名と一致するStructのFieldに値を入れるが、Key名とField名が一致させられない場合は構造体タグの利用を検討する
  • json.Unmarshal で時刻を扱う場合、json.Unmarshal は決め打ちで RFC3339(ISO8601)のフォーマットとしてDecodeを行う。他の形式の時刻データを扱う場合は、独自型を作成し、UnmarshalJSON というメソッドを定義することで対応できる。(他の手もある)
  • 場合によって型が変わる場合は、 mapstructure などのライブラリを使うことで対応できる。

ほか、オススメの記事

Go言語におけるJsonのParseに困ったら、以下の記事が参考になると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0