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するのは気分が悪いときがある)

最悪なにかライブラリがあれば。。

gist