

## 動的に形が変わる JSON

JSON 中の特定のキーの値などに対応してそれ以外のキーの形が変わるという決まりをもった JSON を考えます。たとえば次のようなものです：

### Externally Tagged

[code]

    [
        {"RGB": {"R": 98, "G": 218, "B": 255}},
        {"YCbCr": {"Y": 255, "Cb": 0, "Cr": -10}}
    ]
    
[/code]

### Internally Tagged

[code]

    [
        {"Space": "RGB", "R": 98, "G": 218, "B": 255},
        {"Space": "YCbCr", "Y": 255, "Cb": 0, "Cr": -10}
    ]
    
[/code]

### Adjacently Tagged

[code]

    [
        {"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255}},
        {"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}}
    ]
    
[/code]

※ "○○ Tagged" という言い方は [Rust の serde のドキュメント](https://serde.rs/enum-
representations.html)から借用しました。

このように、特定の値に依存して型が変わるような JSON を Go で unmarshal したい、というのがこの質問の本題です。

## json.RawMessage

このような JSON を unmarshal するための仕組みのひとつとして、encoding/json には
[`json.RawMessage`](https://pkg.go.dev/encoding/json?tab=doc#RawMessage)
という型があります。これは unmarshal する前の生の文字列を保持することで unmarshal を遅延させ、使うときになって必要に応じて
unmarshal させる、という使われ方が想定されているものです。

Adjacently tagged
な場合の具体的なソースコードが[ドキュメントに書かれています](https://pkg.go.dev/encoding/json?tab=doc#example-
RawMessage-Unmarshal)。タグと RawMessage の組として一度 unmarshal し、その後タグで switch して
RawMessage を unmarshal するというものです。

Internally tagged の場合も、たとえば一度タグだけ unmarshal し、その値で switch して再度全体を unmarshal
するという流れで書けます。[サンプルコードはこんな感じです](https://play.golang.org/p/K5mOWvyliog)。全体を 2 回
unmarshal しているのが微妙ではあります。

このように `json.RawMessage` を使った場合のつらいところが、RawMessage としてデータを取り回していると使うたびに
unmarshal する必要があるところです。可能であれば、一度 unmarshal すれば後は unmarshal
せずデータが使い回せるように書きたいです。

## map

スタック・オーバーフローの既存の質問では、map として unmarshal するものも見当たりました。

  * [Goで数字がキーのJsonをUnmarshalしたい](https://ja.stackoverflow.com/q/20497/19110)
  * [キーが変動するJsonをパースしたい](https://ja.stackoverflow.com/q/19393/19110)
  * [How to parse/deserialize dynamic JSON](https://stackoverflow.com/q/29347092/5989200)

しかし map として受け取ってしまうと、折角の型システムの恩恵を受けられません：

  * 不正な値があったとしても unmarshal 時に弾くことができません。たとえば `map[string]interface{}` として unmarshal すると、string が想定されているキーに int が書かれていたとしてもエラーになりません。`map[string]string` として unmarshal すると、int や struct など string 以外が想定されているキーがあると対応できません。
  * また、JSON の形を型によって一箇所にまとめて管理するのも難しくなります。

同じ理由で、`interface{}` として unmarshal し、使うときに型アサーションするのもやや微妙です。

ゆるふわに unmarshal したいときは逆に map や `interface{}`
は便利でしょうが、今回はしっかり見てあげたいです。なおこの方向性だと型アサーション相当のことを良い感じに裏でやってくれる
[dproxy](https://github.com/koron/go-dproxy) や
[jsonpointer](https://github.com/mattn/go-jsonpointer)
というライブラリが知られており、これはこれで便利そうです。

## 質問

タグに依存して動的に形が変わるような JSON を、上手く unmarshal する方法はあるでしょうか？

使うたびに unmarshal するのではなく、一度 unmarshal すればその後 unmarshal
しなくてよい方法が良いです。また可能な限り型の恩恵を受けたく、かつ最初の unmarshal 時にエラーが分かると嬉しいです。

環境: Go 1.14


CC BY-SA 4.0
2020-04-22T03:01:13.403
65838
2020-04-22T10:02:15.090
2020-04-22T10:02:15.090
19110
19110
question
go json
タグによって動的に形が変わる JSON を Go で一括 unmarshal し、値を保持したい


`UnmarshalJSON` メソッドを使う方法を見つけました。このメソッドを生やした型は
[`json.Unmarshaler`](https://pkg.go.dev/encoding/json?tab=doc#Unmarshaler)
として扱われ、`json.Unmarshal` したときに `UnmarshalJSON` が使われるようにカスタムできます。

Internally tagged な場合のサンプルコード：

[code]

    type Color struct {
        Space   string
        Content interface{}
    }
    type RGB struct {
        R uint8
        G uint8
        B uint8
    }
    type YCbCr struct {
        Y  uint8
        Cb int8
        Cr int8
    }
    
    func (c *Color) UnmarshalJSON(data []byte) error {
        // By convention, Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
        // TODO: ↑この文の解釈、こういうことで合ってるんでしょうか……。
        if bytes.Equal(data, []byte("null")) {
            return nil
        }
    
        var space = struct {
            Space string
        }{}
        err := json.Unmarshal(data, &space)
        if err != nil {
            return fmt.Errorf("Space not found: %w", err)
        }
        c.Space = space.Space
    
        switch space.Space {
        case "RGB":
            var rgb RGB
            if err := json.Unmarshal(data, &rgb); err != nil {
                return fmt.Errorf("Space says this is RGB, but cannot unmarshal as RGB: %w", err)
            }
            c.Content = rgb
        case "YCbCr":
            var ycbcr YCbCr
            if err := json.Unmarshal(data, &ycbcr); err != nil {
                return fmt.Errorf("Space says this is YCbCr, but cannot unmarshal as YCbCr: %w", err)
            }
            c.Content = ycbcr
        default:
            return errors.New("Unknown Space: " + space.Space)
        }
    
        return nil
    }
    
    func main() {
        var j = []byte(`[
            {"Space": "YCbCr", "Y": 255, "Cb": 0, "Cr": -10},
            {"Space": "RGB",   "R": 98, "G": 218, "B": 255}
        ]`)
        var colors []Color
        err := json.Unmarshal(j, &colors)
        if err != nil {
            log.Fatal(err)
        }
    
        for _, c := range colors {
            switch c.Space {
            case "RGB":
                rgb, _ := c.Content.(RGB)
                fmt.Println(c.Space, rgb)
            case "YCbCr":
                ycbcr, _ := c.Content.(YCbCr)
                fmt.Println(c.Space, ycbcr)
            }
        }
    }
    
[/code]

これで unmarshal は 1 回になりました。

欠点として、タグを string で比較している点と、必ず成功する型アサーションをしないといけない点があります（実質 tagged union
的なことをしています）。このあたりを何とかできる方法があればコメントや別回答で教えていただけるとありがたいです。go generate
で上手くやるなどありそう……？


CC BY-SA 4.0
2020-04-22T03:01:13.403
65839
2020-04-22T03:48:59.737
2020-04-22T03:48:59.737
19110
19110
65838
answer