読者です 読者をやめる 読者になる 読者になる

初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

スポンサーリンク

Go言語のJSONのデコード(Unmarshal)時に動的に型を切り替えたい



Go!Go!マッソー

Go言語は今時のナウでヤングな言語なので標準でJSONをサポートしているのだけれども、少しだけハマったのでメモ。

汎用的なデータ構造

APIのインターフェイスとかで、大枠は同じ構造なんだけど詳細だけ構造を変えたい、というのはよくある。例えばこんな構造。

  • type.go
type Message struct {
	Id    string      `json:"id"`
	Value interface{} `json:"value"`
}

func (m Message) String() string {
	return fmt.Sprintf("[id:%s][value:%s]", m.Id, m.Value)
}

type DataA struct {
	Attributes map[string]string `json:"attributes"`
}

func (a DataA) String() string {
	return fmt.Sprintf("%v", a.Attributes)
}

type DataB struct {
	Items []string `json:"items"`
}

func (b DataB) String() string {
	return fmt.Sprintf("%v", b.Items)
}

Message構造体のIdに設定されるメッセージID毎にValueに設定されるデータの型が変化するパターン。

Id Value
"msgA" DataA構造体
"msgB" DataB構造体

ヘッダとペイロードを分離してデータ構造を定義できるので結構使われる形式だと思う。

残念な例

で、こいつをJSONエンコード/デコードするサンプルはこんなかんじ。

  • bad.go
package main

import (
	"fmt"
	"encoding/json"
)

func main() {

	dataA := DataA{
		Attributes: map[string]string{
			"aaa": "AAA",
			"bbb": "BBB",
			"ccc": "CCC",
		},
	}
	msgA := Message{
		Id:    "msgA",
		Value: dataA,
	}

	dataB := DataB{
		Items: []string{"111", "222", "333"},
	}
	msgB := Message{
		Id:    "msgB",
		Value: dataB,
	}

	fmt.Printf("%#v\n",msgA)
	jsonA := toJson(msgA)
	fmt.Println(jsonA)
	fmt.Printf("%#v\n",fromJson(jsonA))

	fmt.Printf("%#v\n",msgB)
	jsonB := toJson(msgB)
	fmt.Println(jsonB)
	fmt.Printf("%#v\n",fromJson(jsonB))
}

func toJson( msg Message) string {
	bytes, err := json.Marshal(msg)
	if err != nil {
		panic("marshal fail")
	}
	return string(bytes)
}

func fromJson( jsonString string) *Message {
	msg := &Message{}
	if err := json.Unmarshal([]byte(jsonString), msg); err != nil {
		panic("unmarshal fail")
	}
	return msg
}

実行結果はこうなる。

main.Message{Id:"msgA", Value:main.DataA{Attributes:map[string]string{"bbb":"BBB", "ccc":"CCC", "aaa":"AAA"}}}
{"id":"msgA","value":{"attributes":{"aaa":"AAA","bbb":"BBB","ccc":"CCC"}}}
&main.Message{Id:"msgA", Value:map[string]interface {}{"attributes":map[string]interface {}{"aaa":"AAA", "bbb":"BBB", "ccc":"CCC"}}}

main.Message{Id:"msgB", Value:main.DataB{Items:[]string{"111", "222", "333"}}}
{"id":"msgB","value":{"items":["111","222","333"]}}
&main.Message{Id:"msgB", Value:map[string]interface {}{"items":[]interface {}{"111", "222", "333"}}}

ぱっとみうまく言ってそうにみえるけど、実はMessage構造体のValueの型がJSONエンコード前とJSONデコード後で異なる。

msgA

JSONエンコード前 DataA構造体
JSONデコード後 map[string]interface{}。さらにその下も同じ

msgA

JSONエンコード前 DataB構造体
JSONデコード後 map[string]interface{}。さらにその下はinterface{}配列

これは、デコード(Unmarshal)時にMessage構造体を渡している為。幸いにも、Mapや配列というデータ構造自体は変化していないのでinterface{}をキャストするなりすればなんとかなるのだが、

面倒くさい。

解決策

ということで、なにか良い方法はないかと検索してみると以下にやり方が書いてあった。

Dynamic JSON in Go

上記の記事によると、そういうときはinterface{}に「json.RawMessage」を指定してやればデコード対象外となるようだ。なぜそうなるかは、「json.RawMessage」の定義をみると理解できる。

  • encoding/json/stream.go
// RawMessage is a raw encoded JSON value.
// It implements Marshaler and Unmarshaler and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte

そう、ただのバイト配列だ。この型が指定された場合はデコード/エンコードしないように実装されているらしい。つまり遅延実行用の型ということだ。

素敵な例

解決策を用いてJSONデコードするのはこんな感じ。
Message構造体のValueにjson.RawMessageを設定してからデコード(Unmarshal)。Idはデコードされているのでそれで処理を分岐してId毎に異なる構造体でさらにデコードする。

  • good.go
func fromJson( jsonString string) *Message {
	var v json.RawMessage
	msg := &Message{ Value: &v }
	if err := json.Unmarshal([]byte(jsonString), msg); err != nil {
		panic("unmarshal fail")
	}
	switch msg.Id {
	case "msgA":
		dataA := &DataA{}
		if err := json.Unmarshal(v, dataA); err != nil {
			panic("unmarshal DataA fail")
		}
		msg.Value = *dataA
	case "msgB":
		dataB := &DataB{}
		if err := json.Unmarshal(v, dataB); err != nil {
			panic("unmarshal DataB fail")
		}
		msg.Value = *dataB
	}
	return msg
}

実行結果はこうなる。

main.Message{Id:"msgA", Value:main.DataA{Attributes:map[string]string{"bbb":"BBB", "ccc":"CCC", "aaa":"AAA"}}}
{"id":"msgA","value":{"attributes":{"aaa":"AAA","bbb":"BBB","ccc":"CCC"}}}
&main.Message{Id:"msgA", Value:main.DataA{Attributes:map[string]string{"aaa":"AAA", "bbb":"BBB", "ccc":"CCC"}}}
main.Message{Id:"msgB", Value:main.DataB{Items:[]string{"111", "222", "333"}}}
{"id":"msgB","value":{"items":["111","222","333"]}}
&main.Message{Id:"msgB", Value:main.DataB{Items:[]string{"111", "222", "333"}}}

このサンプルを書いた後に、Message構造体のValueに最終的にデコードしたい型(例えばDataA)を設定してからデコードしたら全部デコードされるんじゃねえかと思ったけれども、ダメでした。ということで、

遅延実行最高。

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

スポンサーリンク