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 が立っています:
github.com
github.com
つまり tinygo 上で任意の struct を JSON にしたい時は「手で気を付けてシリアル化する」しか方法がなかったわけですが,まあそれだと何かと不便だったので表題の通り json-ice という encoding/json
に依存しない JSON marshaler のコードジェネレータを作りました.
github.com
挙動としては,事前に marshaling 対象となる struct (の json
カスタム struct タグ) を解釈して JSON に marshal するコードを吐き出す,という至ってシンプルなものとなります.似たような挙動をする先行実装に mailru/easyjson などがありますが,これらは内部的に encoding/json
に依存しているようで,今回の用途にはマッチしませんでした.
例えば以下のような struct を marshal したい時には go:generate
と一緒にコードを書いておくと
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)
これにより実行時に 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.
github.com
余談ですが
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)
生成コードはこんな感じ
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
}