goでadditionalProperties:trueのように余分なデータを保持したままJSONをmarshal/unmarshalする良い方法がみつからない
時折、余分なデータを保持したままJSONをmarshal/unmarshalしたくなることがある。例えばwebAPIを利用するときなどに、すべてのフィールドを記述するのは面倒だけれど、新しく増えるフィールドの存在に気づかず欠損してしまうのは避けたいみたいな状況。
内部的なクライアントライブラリのようなコードを書きたいときに、この種の思いが頭をよぎる。
ふつうのJSONのmarshall/unmarshal
普通は構造体に含まれない余分なフィールドの値は消える。
例えば以下のようなnameとageだけをフィールドとして持つ構造体を定義し、これに値を埋め込む形でmarshal/unmarshalした場合には、当然ではあるけれど、余分なフィールドは消えてなくなる。
// Person ... type Person struct { Name string `json:"name"` Age int `json:"age"` }
nicknameというフィールドも元のJSONには存在する。
$ go run 00loaddump/main.go input: { "name": "foo", "age": 20, "nickname": "F" } got: main.Person{Name:"foo", Age:20} output: {"name":"foo","age":20}
このときのコードは以下。
00loaddump/main.go
package main import ( "bytes" "encoding/json" "fmt" "log" "os" ) // Person ... type Person struct { Name string `json:"name"` Age int `json:"age"` } func main() { if err := run(); err != nil { log.Fatalf("!%+v", err) } } func run() error { code := ` { "name": "foo", "age": 20, "nickname": "F" } ` fmt.Println("input:") fmt.Println(code) fmt.Println("") o := bytes.NewBufferString(code) decoder := json.NewDecoder(o) var ob Person if err := decoder.Decode(&ob); err != nil { return err } fmt.Println("got:") fmt.Printf("%#+v\n", ob) fmt.Println("") fmt.Println("output:") encoder := json.NewEncoder(os.Stdout) if err := encoder.Encode(&ob); err != nil { return err } return nil }
余分なデータを保持したmarshal/unmarshal
さて、このような場合に手軽に余分なデータを保持しておけるならちょっとした手間でもやっておきたい。こういうような振る舞いのもの。
$ go run 01loaddump/main.go input: { "name": "foo", "age": 20, "nickname": "F" } got: main.Person{Name:"foo", Age:20, AdditionalProperties:map[string]interface {}{"nickname":"F"}} output: {"name":"foo","age":20,"nickname":"F"}
ここでデータ自体は以下の様な形状のもの。nicknameはフィールド定義には含まれない。
// Person ... type Person struct { Name string `json:"name"` Age int `json:"age"` // data additional properties AdditionalProperties map[string]interface{} `json:"-"` }
しかし正攻法を考えると以下のようにコードが途方も無く膨らむ。
UnmarshalJSON()
を自分で定義する必要があり、そして自分で記述した部分のフィールドだけからなる構造体を別途内部で作る必要が出てくる。実際のコードは以下の様な形になる(go-swaggerが出力したコードはこのような形になる)。
さすがにこれは、手で記述するには、あまりにもつらい。
// Person ... type Person struct { Name string `json:"name"` Age int `json:"age"` // data additional properties AdditionalProperties map[string]interface{} `json:"-"` } // UnmarshalJSON unmarshals this object with additional properties from JSON func (m *Person) UnmarshalJSON(data []byte) error { // stage 1, bind the properties var stage1 struct { Name string `json:"name"` Age int `json:"age"` } if err := json.Unmarshal(data, &stage1); err != nil { return err } var rcv Person rcv.Name = stage1.Name rcv.Age = stage1.Age *m = rcv // stage 2, remove properties and add to map stage2 := make(map[string]json.RawMessage) if err := json.Unmarshal(data, &stage2); err != nil { return err } delete(stage2, "name") delete(stage2, "age") // stage 3, add additional properties values if len(stage2) > 0 { result := make(map[string]interface{}) for k, v := range stage2 { var toadd interface{} if err := json.Unmarshal(v, &toadd); err != nil { return err } result[k] = toadd } m.AdditionalProperties = result } return nil } // MarshalJSON marshals this object with additional properties into a JSON object func (m Person) MarshalJSON() ([]byte, error) { var stage1 struct { Name string `json:"name"` Age int `json:"age"` } stage1.Name = m.Name stage1.Age = m.Age // make JSON object for known properties props, err := json.Marshal(stage1) if err != nil { return nil, err } if len(m.AdditionalProperties) == 0 { return props, nil } // make JSON object for the additional properties additional, err := json.Marshal(m.AdditionalProperties) if err != nil { return nil, err } if len(props) < 3 { return additional, nil } // concatenate the 2 objects props[len(props)-1] = ',' return append(props, additional[1:]...), nil }
遅くても良いのであればreflectで一定逃げられるが、それでもやっぱりsubsetとなるようなフィールドは定義してあげる必要がある。そして全部をreflectで受ければすべてreflectで記述する必要が出てくる。辛い。
どうしたら気づけるかというとあまり方法がなく、通信結果を記録できるようなroundTripper()などを挟めるようにしておいたり、interceptor的なものを挟めるようにしておくくらいしかない。
(もちろんOpenAPI docを書いておいてコード生成するなどの方法もあるが、本当に一部のAPIだけを利用するために数千行のコードをimportするのは気分が悪いときがある)
最悪なにかライブラリがあれば。。