野良 HTTP JSON API クライアントを作ってると、API が返してくる JSON の形に確信が持てないし、「これ何に使うんだろ」みたいなフィールドもあったりして struct にエンコードするのをサボったりする。
そういったコードがライブラリとして使われる余地を残すとすると、struct で表現されていないデータにも何らかの方法でアクセスできるようにしておきたい。こういうパターンあるんじゃないかと思うが、みんなどうやってるのか分からなかったのでメモ。
まあ素直に、json.RawMessage を struct に持たせておくのが一番よいだろう。冗長にはなるが、構造体の定義されたフィールドに便利なデータはあるし、より詳細に見たいなら RawMessage 経由で生データを見ればよい。ということにする。また、RawMessage を保持している場合は JSON 化したときにこれをそのまま使いたい。
問題は json.Marhsaler/Unmarshaler の実装だ。まず前提として JSON エンコード・デコードを手書きしたくはない。encoding/json の実装に乗っかりたい。前記の要件を満たすため MarshalJSON/UnmarshalJSON を自作することになるが、そうなると encoding/json の実装をそのまま使うことができない。
そこで型をもう一つ用意することにする。同じ構造体をベースに、json.Marshaler/Unmarshaler を実装しない別の型を作って、これを経由して JSON 化をおこなう。
type Struct struct { rawJSON json.RawMessage } type _struct Struct func (s Struct) MarshalJSON() ([]byte, error) { if s.rawJSON != nil { return json.Marshal(s.rawJSON) } return json.Marshal(_struct(s)) } func (s *Struct) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, (*_struct)(s)) if err != nil { return err } s.rawJSON = append([]byte{}, data...) return nil }
凝った形状の構造体を作ったり、reflect を使ったりする方法もあるだろうけど、できるだけ素直にやるならこんな感じかなあ。