tinygo 向けの JSON marshaler: go-json-ice を書いた
English article is here: Released go-json-ice: a code generator of JSON marshaler for tinygo - moznion's tech blog
tinygo では encoding/json
を import するとコンパイルできなくなるという問題があり *1,なんらかの struct を JSON に marshal したい時に使える de facto な方法が無いように見えました.これに関しては例えば以下のような issue が立っています:
つまり tinygo 上で任意の struct を JSON にしたい時は「手で気を付けてシリアル化する」しか方法がなかったわけですが,まあそれだと何かと不便だったので表題の通り json-ice という encoding/json
に依存しない JSON marshaler のコードジェネレータを作りました.
挙動としては,事前に marshaling 対象となる struct (の json
カスタム struct タグ) を解釈して JSON に marshal するコードを吐き出す,という至ってシンプルなものとなります.似たような挙動をする先行実装に mailru/easyjson などがありますが,これらは内部的に encoding/json
に依存しているようで,今回の用途にはマッチしませんでした.
例えば以下のような struct を marshal したい時には go:generate
と一緒にコードを書いておくと
//go:generate json-ice --type=AwesomeStruct type AwesomeStruct struct { Foo string `json:"foo"` Bar string `json:"bar,omitempty"` }
MarshalAwesomeStructAsJSON(s *AwesomeStruct) ([]byte, error)
というコードが生成されるので,それを利用して struct を JSON に marshal することが可能です:
marshaled, err := MarshalAwesomeStructAsJSON(&AwesomeStruct{ Foo: "buz", Bar: "", }) if err != nil { log.Fatal(err) } fmt.Printf("%s\n", marshaled) // => {"foo":"buz"}
これにより実行時に reflection を使って動的に marshaling する必要がなくなるので,tinygo でも JSON marshaling が簡単に行えるようになります.また,動的な reflection の代わりに事前計算するので当然の結果ですがパフォーマンスも少し良くなります *2.
もちろんその副作用として interface{}
な値を持つ struct については動的な型の解決ができないため marshaling ができません.Marshaling するためには静的に型の解決ができる必要があります.
また tinygo は wasm を吐き出す機能も有しており,この wasm が実行時に import
するモジュールは「元のコードが何に依存しているか」によって変化してきます.この実行時の依存をできる限りミニマムにしたい (例えばブラウザランタイム以外の強力な sandbox 環境で wasm を動かすというユースケースが考えられる) という動機があったので,生成コードが依存するパッケージは可能な限り最小限にとどめました.結果的に現時点では strconv
にのみ依存するようになっています.ミニマル!
そんな感じのライブラリです.どうぞご利用ください!
もちろん tinygo ではなく通常の go の処理系でも利用できますが,それについてはもっと良い先行実装 (それこそ easyjson とか) があると思うので,そちらの利用の検討をおすすめします.
なお,この実装は JSON の marshaling のみをサポートするものですが,逆に tinygo で JSON unmarshaling するにはどうすればよいかと言うと,buger/jsonparser を利用すれば良いように思いました.
> It does not rely on encoding/json, reflection or interface{}, the only real package dependency is bytes.
余談ですが
//go:generate json-ice --type=DeepStruct type DeepStruct struct { Deep []map[string]map[string]map[string]map[string]string `json:"deep"` }
のような深く,再帰的(?)な型についてもちゃんとしたコード生成が可能です:
given := &DeepStruct{ Deep: []map[string]map[string]map[string]map[string]string{ { "foo": { "bar": { "buz": { "qux": "foobar", }, }, }, }, { "foofoo": { "barbar": { "buzbuz": { "quxqux": "foobarfoobar", }, }, }, }, }, } marshaled, err := MarshalDeepStructAsJSON(given) if err != nil { log.Fatal(err) } log.Printf("[debug] %s", marshaled) // => {"deep":[{"foo":{"bar":{"buz":{"qux":"foobar"}}}},{"foofoo":{"barbar":{"buzbuz":{"quxqux":"foobarfoobar"}}}}]}
生成コードはこんな感じ
import "github.com/moznion/go-json-ice/serializer" func MarshalDeepStructAsJSON(s *DeepStruct) ([]byte, error) { buff := make([]byte, 1, 54) buff[0] = '{' if s.Deep == nil { buff = append(buff, "\"deep\":null,"...) } else { buff = append(buff, "\"deep\":"...) buff = append(buff, '[') for _, v := range s.Deep { if v == nil { buff = append(buff, "null"...) } else { buff = append(buff, '{') for mapKey, mapValue := range v { buff = serializer.AppendSerializedString(buff, mapKey) buff = append(buff, ':') if mapValue == nil { buff = append(buff, "null"...) } else { buff = append(buff, '{') for mapKey, mapValue := range mapValue { buff = serializer.AppendSerializedString(buff, mapKey) buff = append(buff, ':') if mapValue == nil { buff = append(buff, "null"...) } else { buff = append(buff, '{') for mapKey, mapValue := range mapValue { buff = serializer.AppendSerializedString(buff, mapKey) buff = append(buff, ':') if mapValue == nil { buff = append(buff, "null"...) } else { buff = append(buff, '{') for mapKey, mapValue := range mapValue { buff = serializer.AppendSerializedString(buff, mapKey) buff = append(buff, ':') buff = serializer.AppendSerializedString(buff, mapValue) buff = append(buff, ',') } if buff[len(buff)-1] == ',' { buff[len(buff)-1] = '}' } else { buff = append(buff, '}') } } buff = append(buff, ',') } if buff[len(buff)-1] == ',' { buff[len(buff)-1] = '}' } else { buff = append(buff, '}') } } buff = append(buff, ',') } if buff[len(buff)-1] == ',' { buff[len(buff)-1] = '}' } else { buff = append(buff, '}') } } buff = append(buff, ',') } if buff[len(buff)-1] == ',' { buff[len(buff)-1] = '}' } else { buff = append(buff, '}') } } buff = append(buff, ',') } if buff[len(buff)-1] == ',' { buff[len(buff)-1] = ']' } else { buff = append(buff, ']') } buff = append(buff, ',') } if buff[len(buff)-1] == ',' { buff[len(buff)-1] = '}' } else { buff = append(buff, '}') } return buff, nil }
*1:reflection 周りのサポートが十分でないため: https://tinygo.org/lang-support/stdlib/#encoding-json