Skip to content

Go で構造の一部が動的に変わる JSON を扱いたい

Posted on:2018年1月3日

json.RawMessage を使うと,一部のフィールドを見てから payload の型を決定することができます

問題

WebSocket でやりとりするサーバを書いていて,一つのコネクション上でいくつかの種類のコマンドを JSON として受け付けるような仕組みが欲しくなりました.

{
  "action": "increment",
  "payload": {
    "value": 3
  }
}

{
  "action": "greet",
  "payload": {
    "name": "World"
    "language": "English"
  }
}

のように, action フィールドに応じて payload の構造が変わるという構成です.

golang でふつうに JSON を受け取って構造体にマップする際には,下のようにします.

type Message struct {
  Action string `json:"action"`
  Payload struct {
    Value int `json:"value"`
  } `json:"payload"`
}
var msg Message
json.Unmarshal(body, &msg)

この方法では, action の値に応じて payload の構造が変わることを表現できません.無理やり Payload の struct を大きくして

struct {
  Value int `json:"value"`
  Name string `json:"name"`
  Language string `json:"language"`
}

のようにすることも出来ますが,どの action の時にどのフィールドが使えるのかわかりにくい上に, value のような汎用的な名前のフィールドが複数の型を取るケースを表現できません.( { "value": 1 }{ "value": "World" } など)

解決方法

json.RawMessage を使って,動的な部分の unmarshal を遅延します.

type Message struct {
  Action string `json:"action"`
  Payload json.RawMessage `json:"payload"`
}
var msg Message
json.Unmarshal(body, &msg)

このようにすると, msg.Payload には []byte(`{"value": 1}`) がそのまま残ります.

そこで,それぞれの action ごとの payload 型を定義しておき

type IncrementPayload struct {
  Value int `json:"value"`
}
type GreetPayload struct {
  Name string `json:"name"`
  Language string `json:"language"`
}

msg.Action に応じて, msg.Payload をどう unmarshal するか決定します.

switch msg.Action {
case "increment":
  var p IncrementPayload
  json.Unmarshal(msg.Payload, &p)
  // do increment action
case "greet":
  var p GreetPayload
  json.Unmarshal(msg.Payload, &p)
  // do greet action
}

まとめ

動的に構造が決定する部分を何かしらの interface として抽象化できる場合は,特にこの手法が有効そうです.

そもそも { "action": "increment", "increment_payload": { "value": 1 } }{ "action": "greet", "greet_payload": { "name": "world", "language": "English" } } のようにフィールド名を変えてしまうという手もありますね.