エラーの内容
JSONをGoの構造体にデコードしていると、突然このようなエラーが発生することがあります:
json: cannot unmarshal string into Go value of type int
json: cannot unmarshal number into Go value of type string
json: cannot unmarshal object into Go value of type []string
このエラーは json.Unmarshal や json.NewDecoder(...).Decode(...) の内部で発生します。型が一致しないフィールドが一つあるだけで処理全体が失敗し、何もデコードされません。
原因
Goの encoding/json パッケージは型の自動変換を行いません。構造体が int を期待しているのに、JSONが "42" というクォートされた文字列を渡すと、デコーダーはその変換を拒否します。これは逆のケースでも同様で、JSONのベアな数値は string フィールドに黙って入ることはありません。
このエラーが最も頻繁に発生する3つのケースを挙げます:
- 数値を文字列として返すサードパーティAPI(例:
"id": 123ではなく"id": "123")— 決済APIや古いRESTサービスでは驚くほど多く見られます - すべてのカラムを文字列としてシリアライズするレガシーデータベースやシステム
- JavaScriptがデータ送信前に数値をクォートされた値に変換するフロントエンドのフォームデータ
修正手順
Step 1 — 型が一致しないフィールドを特定する
エラーメッセージにはGoが期待していた型が含まれています。それを構造体と照合して問題のフィールドを特定しましょう。具体的な例を示します:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// APIからの受信JSON:
// {"id": "99", "name": "Alice", "age": 30}
// ^ JSONでは"id"が文字列だが構造体ではint → エラーが発生する
Step 2 — 方法A:JSONのソースを修正する(推奨)
APIやデータソースを制御できる場合は、そこで修正しましょう。JSONでは数値はクォートなしで記述するのが正しい形式です:
// 誤り
{"id": "99", "age": "30"}
// 正しい
{"id": 99, "age": 30}
根本から解決すれば、回避策は一切不要です。
Step 3 — 方法B:文字列フィールドを使用して手動で変換する
ソースを変更できない場合は、構造体のフィールドを string に変更してクリーンにアンマーシャルし、その後自分で変換します:
import (
"encoding/json"
"fmt"
"strconv"
)
type User struct {
ID string `json:"id"` // JSONから文字列として受け取る
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
// アンマーシャル後に変換する
id, err := strconv.Atoi(u.ID)
if err != nil {
panic(fmt.Sprintf("invalid id: %v", err))
}
fmt.Println(id) // 99
}
Step 4 — 方法C:柔軟な数値フィールドにjson.Numberを使用する
json.Number は内部的には文字列型で、任意のJSON数値を保持できます。実際の値は明示的に取り出します:
import (
"encoding/json"
"fmt"
)
type User struct {
ID json.Number `json:"id"`
Name string `json:"name"`
}
func main() {
data := []byte(`{"id": 99, "name": "Alice"}`)
var u User
json.Unmarshal(data, &u)
id, _ := u.ID.Int64()
fmt.Println(id) // 99
}
Step 5 — 方法D:カスタムUnmarshalJSONを実装する
APIによっては一貫性がなく、あるレスポンスでは "id" が 99 として、別のレスポンスでは "99" として返ってくることがあります。カスタムアンマーシャラーを使えば両方のケースに対応できます:
import (
"encoding/json"
"strconv"
)
type FlexInt int
func (f *FlexInt) UnmarshalJSON(b []byte) error {
// まず数値として試みる
var n int
if err := json.Unmarshal(b, &n); err == nil {
*f = FlexInt(n)
return nil
}
// 文字列にフォールバック
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
n, err := strconv.Atoi(s)
if err != nil {
return err
}
*f = FlexInt(n)
return nil
}
type User struct {
ID FlexInt `json:"id"`
Name string `json:"name"`
}
func main() {
// {"id": 99}と{"id": "99"}の両方に対応
data := []byte(`{"id": "99", "name": "Alice"}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
// u.ID は 99
}
修正の確認
最も簡単な確認方法は、デコード後に err を検査することです:
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Println("still broken:", err)
} else {
fmt.Printf("decoded: %+v\n", u)
}
さらに良い方法として、ユニットテストを書くことをお勧めします。リグレッションを自動的に検出できます:
func TestDecodeUser(t *testing.T) {
data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
t.Fatal(err)
}
if int(u.ID) != 99 {
t.Fatalf("expected 99, got %d", u.ID)
}
}
補足情報
構造体を定義する前にJSONペイロードを確認する
APIのレスポンスは往々にして乱雑です。Goの構造体を一つも定義する前に、JSONをフォーマットして内容を確認しましょう。私は ToolCraftのJSON Formatter を使っています。生のレスポンスを貼り付けると、どの数値がクォートされていてどれがそうでないかをすぐに確認できます。すべてブラウザ上で動作し、データはアップロードされないため、ペイロードにユーザーデータが含まれている場合でも安心して使えます。
ネストされたフィールドに真の原因が隠れていることがある
型の不一致は必ずしもトップレベルで発生するとは限りません。3階層深いフィールドでも同じエラーが発生します。エラーの型を出力すると詳細な情報が得られます:
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Printf("type: %T, detail: %v\n", err, err)
}
構造体タグは大文字・小文字を区別する
json:"fieldname" タグはJSONのキーと大文字・小文字まで完全に一致している必要があります。Go の構造体フィールドは慣例としてエクスポート(大文字始まり)されていますが、REST APIはほぼ常に小文字のキーを送信します。json:"UserID" というタグはペイロード内の "userid" とは一致しません。
逃げ道としてinterface{}を使わない
map[string]interface{} にデコードするとこのエラーは回避できます。しかしコストがあります。数値は暗黙的に float64 になり、型安全性が完全に失われ、その後のフィールドアクセスはすべて型アサーションが必要になります。型付き構造体は最初は冗長に見えますが、後々はるかに扱いやすくなります。

