詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Goで知らないフィールドのあるJSONを取り扱う

野良 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 を使ったりする方法もあるだろうけど、できるだけ素直にやるならこんな感じかなあ。

動く例: https://go.dev/play/p/gdQ4U6W34ef

はてなで一緒に働きませんか?