はじめに (対象読者・この記事でわかること)
この記事は、「JSON のフィールドの型が条件によって変わってしまう Web API に接続しなければならない」という状況に直面している Go エンジニアを対象としています。
具体的には、同じフィールドが文字列だったり数値だったり、さらにオブジェクトになることもあるような「型不定」なレスポンスを、一括で構造体へ unmarshal しつつ、後から型安全に値を取り出したい方です。
この記事を読むことで
interface{}を活用して「どんな型でも一旦受け入れる」設計の仕方- 型アサーションと型スイッチで実行時に安全に値を取り出す実装パターン
- 一つの構造体で複数の型を扱いつつ、ビジネスロジック側で「どの型でも扱いやすくする」ヘルパーの書き方
が身に付きます。
サンプルコードはそのままコピペ+αで動く構成になっているので、すぐにプロダクトへ転用できるでしょう。
前提知識
- Go の構造体タグ(
json:"name")の基本 encoding/jsonパッケージを使った unmarshal の経験- インターフェース(interface)と型アサーション(
v.(T))の存在を知っていること
なぜ「型が変わる JSON」がつらいのか
近年の Web API では「バージョン違いでフィールドの型が変わる」「エラー時は文字列、正常時はオブジェクト」といった仕様に遭遇することがあります。
Go らしく構造体を定義して unmarshal しようとすると「型が違う」旨のエラーで失敗します。
そこで「どんな型でも入れられる箱」が必要になり、それが interface{} です。
本記事では、interface{} を経由して「一旦受け入れ→後から型を見て処理」を可能にする設計を紹介します。
interface{}+型スイッチで型不定 JSON を一括 unmarshal する
ステップ 1:共通の入れ物を作る
まず「型が変わりうるフィールド」を interface{} で宣言します。
他のフィールドは普通に string / int などで OK です。
Gopackage main import ( "encoding/json" "fmt" "log" ) type Event struct { ID string `json:"id"` Type string `json:"type"` Status interface{} `json:"status"` // ここが不定 CreatedAt int64 `json:"created_at"` }
ステップ 2:一括で unmarshal してみる
Gofunc main() { // 例1: status が文字列 jsonStr1 := `{"id":"1","type":"order","status":"running","created_at":1680000000}` // 例2: status がオブジェクト jsonStr2 := `{"id":"2","type":"order","status":{"progress":80,"eta":"10m"},"created_at":1680001000}` for _, src := range []string{jsonStr1, jsonStr2} { var e Event if err := json.Unmarshal([]byte(src), &e); err != nil { log.Fatal(err) } fmt.Printf("ID=%s, raw status=%v\n", e.ID, e.Status) } }
この時点で unmarshal は成功し、e.Status には
- 例1 では string 型
- 例2 では map[string]interface{} 型
が入っています。
ステップ 3:型スイッチで安全に値を取り出す
ビジネスロジック側で「status が進行中か?」を判定したい場合を考えます。
Gofunc IsRunning(st interface{}) bool { switch v := st.(type) { case string: return v == "running" case map[string]interface{}: // オブジェクト版 return v["progress"] != nil && v["progress"] != float64(0) default: return false } }
Gofmt.Println("running?", IsRunning(e.Status))
ステップ 4:ヘルパーメソッドで構造体に振る舞いを持たせる
各所で型スイッチを書くと重複するので、構造体メソッド化します。
Gofunc (e Event) IsRunning() bool { return IsRunning(e.Status) } func (e Event) ProgressPercent() (int, bool) { switch v := e.Status.(type) { case map[string]interface{}: if p, ok := v["progress"].(float64); ok { return int(p), true } } return 0, false }
これで呼び出し側は
Goif e.IsRunning() { if p, ok := e.ProgressPercent(); ok { fmt.Printf("進行中: %d%%\n", p) } }
とシンプルに書けます。
ステップ 5:カスタム unmarshal を併用したい場合
「status を必ず string にしたい」「オブジェクトの場合は JSON 文字列化して保持したい」という要件なら、カスタム型に UnmarshalJSON を実装します。
Gotype FlexibleStatus struct { Value interface{} } func (fs *FlexibleStatus) UnmarshalJSON(data []byte) error { // 1. まず文字列としてトライ if data[0] == '"' { var s string if err := json.Unmarshal(data, &s); err != nil { return err } fs.Value = s return nil } // 2. オブジェクト or 配列 var m interface{} if err := json.Unmarshal(data, &m); err != nil { return err } fs.Value = m return nil }
Event.Status を FlexibleStatus 型に置き換えるだけで、独自ルールを適用できます。
ハマりがちなポイントと解決策
-
数値が float64 になる
JSON 仕様上、数値はすべて float64 扱い。v.(int)とアサーションすると panic します。
→v.(float64)で一旦受けてからint(v)するか、json.Numberを使う -
nil チェックを怠ると panic
型アサーションはv.(T)のみだと panic します。
→v, ok := v.(T)の 2 値返却フォームを使い、!ok時の分岐を書く -
map キーが存在しないとゼロ値
m["progress"]が存在しない場合のゼロ値と、進捗 0 を区別できない。
→ 構造体にマッピングする際は「存在フラグ」を別途持つ or*intにしてポインタで有無を表現
まとめ
本記事では、Go において「フィールドの型がブレる JSON」を interface{} で一旦受け入れ、型スイッチ+型アサーションで実行時に安全に値を取り出す方法を解説しました。
interface{}を使えば unmarshal 失敗を回避できる- 型スイッチで分岐し、ビジネスロジックに合わせたヘルパーを書くことで、呼び出し側はシンプルに書ける
- 数値は float64 になる、nil チェックは必須、などの落とし穴を抑えれば実装は安定する
このテクニックを使えば、サードパーティ API の破壊的変更にも「とりあえず動く」状態を保ちながら、段階的に型を厳格にしていく、という運用が可能になります。
次回は「型が確定したら generics を使ってより厳格に扱う」アプローチを紹介する予定です。
参考資料
- encoding/json — Go 標準パッケージ
- Effective Go - Interface
- Go by Example: Type Assertions
- Go Web プログラミング実践入門(型不定レスポンスの例あり)
