Goの'json: cannot unmarshal string into Go value of type int'エラーの修正

intermediate🔷 Go2026-03-23| Go 1.16以降、任意のOS(Linux、macOS、Windows)、REST APIの利用やJSONの設定ファイル読み込み時によく発生

Error Message

json: cannot unmarshal string into Go value of type int
#json#アンマーシャル#構造体#エンコーディング#api

エラーの内容

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.Unmarshaljson.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 になり、型安全性が完全に失われ、その後のフィールドアクセスはすべて型アサーションが必要になります。型付き構造体は最初は冗長に見えますが、後々はるかに扱いやすくなります。

Related Error Notes