GoでJSONを扱うには、標準ライブラリの encoding/json を使い、構造体タグ(json:"name,omitempty" のような注釈)でキー名や省略の条件を指定します。 外部ライブラリなしで、構造体と JSON の相互変換、ストリームの読み書き、独自の変換ルールまで書けます。

2026年8月リリースの Go 1.27 では、新しい encoding/json/v2 と encoding/json/jsontext が正式なパッケージになり、従来の encoding/json も内部が v2 の実装に置き換わりました。 この記事では、Go 1.27.1 で全コードを実行し、出力とエラー文をそのまま載せています。 構造体タグの全オプションの出力、v1 と v2 で結果が変わるケース(nil スライス・重複キー・大文字小文字・不正な UTF-8 など)を実行結果で比べられます。

Go JSONの早見表

構造体タグの一覧

タグの書き方 意味 v1(encoding/json) v2(encoding/json/v2)
json:"id" キー名を変える ○ ○
タグなし フィールド名がそのままキーになる(NoTag) ○ ○
json:"-" 常に出力しない・読み込まない ○ ○
json:"-," キー名を - にする ○ ○
json:"note,omitempty" 空なら省略 false・0・nil・空の文字列/スライス/map JSON で null・""・{}・[] になる値
json:"price,omitzero" Go のゼロ値(または IsZero() が true)なら省略 ○(Go 1.24〜) ○
json:"code,string" 値を文字列に入れて出す("123") 数値・bool・文字列に使える 数値だけに使える
json:"title,case:ignore" 読み込み時に大文字小文字を区別しない ― ○
json:"title,case:strict" 読み込み時に大文字小文字を区別する ― ○
json:",embed" 中の構造体を親に展開する/未知のキーを受け止める ― ○(旧 inline)
小文字始まりのフィールド 出力も読み込みもされない 無視 無視(全フィールドが小文字だとエラー)

関数・型の逆引き

やりたいこと v1(encoding/json) v2(encoding/json/v2)
構造体 → JSON([]byte) json.Marshal(v) json.Marshal(v, opts...)
整形して出力 json.MarshalIndent(v, "", " ") json.Marshal(v, jsontext.WithIndent(" "))
JSON → 構造体 json.Unmarshal(b, &v) json.Unmarshal(b, &v, opts...)
io.Writer に書く json.NewEncoder(w).Encode(v) json.MarshalWrite(w, v)
io.Reader から読む json.NewDecoder(r).Decode(&v) json.UnmarshalRead(r, &v)
形の決まっていない JSON map[string]any map[string]any
一部を後で解釈する json.RawMessage jsontext.Value(v1 の RawMessage と同じ型)
大きな数値を正確に Decoder.UseNumber() と json.Number jsontext.Value などで受ける
変換ルールを自作 MarshalJSON / UnmarshalJSON メソッド 同左+MarshalJSONTo / UnmarshalJSONFrom
未知のキーをエラーに Decoder.DisallowUnknownFields() json.RejectUnknownMembers(true)
JSON として正しいか json.Valid(b) jsontext.Value(b).IsValid()

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Go JSON|構造体タグ一覧とencoding/json v2の変更点」

検証環境とGo 1.27のencoding/jsonの状況

項目 内容
Go go1.27.1 linux/amd64
外部モジュール なし(標準ライブラリのみ)
実行方法 go mod init example.com/jsondemo → go run .
タイムゾーン Asia/Tokyo(time.Time の表示に影響)

Go 1.27 のリリースノートで確認できる、JSON まわりの変更点は次のとおりです。

変更点 内容
新パッケージ encoding/json/v2(意味の変換:Go の値 ⇔ JSON)と encoding/json/jsontext(文法の処理:トークン・整形)が使えるようになった
既存の encoding/json 内部実装が v2 に置き換わった。 Marshal・Unmarshal の動作は保たれるが、エラーメッセージの文言は変わる場合がある。Unmarshal は大きく高速化
v1 の継続 v1 の API は今後もサポートされ、移行は必須ではない
元に戻す方法 ビルド時に GOEXPERIMENT=nojsonv2 で従来の実装に戻せる(将来のリリースで削除予定)
v2 の既定 不正な UTF-8 と重複したキーをエラーにする、より厳格な既定値
実験期間からの変更 inline タグは embed に改名、format タグと unknown タグは削除

Go 1.25 から GOEXPERIMENT=jsonv2 を付けたときだけ試せる実験機能として提供されていた v2 が、1.27 では何も指定しなくても import できます。 なお、この記事の v1 のエラー文は Go 1.27.1 の既定の実装で出したものです。GOEXPERIMENT=nojsonv2 で同じコードを実行しても、この記事の範囲では同じ文言でした。

構造体タグの全オプションと出力

v1(encoding/json)で、タグごとの出力を確かめます。値が入っている場合と、空の場合の2パターンを出力します。

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Item struct {
    ID        int       `json:"id"`                   // 名前を変える
    Name      string    `json:"name"`
    Note      string    `json:"note,omitempty"`       // 空文字なら省略
    Tags      []string  `json:"tags,omitempty"`       // nil・空スライスなら省略
    Stock     int       `json:"stock,omitempty"`      // 0 なら省略(v1)
    Price     int       `json:"price,omitzero"`       // ゼロ値なら省略(Go 1.24〜)
    CreatedAt time.Time `json:"created_at,omitzero"`  // IsZero() が true なら省略
    UpdatedAt time.Time `json:"updated_at,omitempty"` // 構造体は omitempty では省略されない
    Code      int64     `json:"code,string"`          // 数値を文字列 "123" として出す
    Secret    string    `json:"-"`                    // 常に出さない
    Dash      string    `json:"-,"`                   // キー名を "-" にする
    NoTag     string                                  // タグなし → フィールド名そのまま
    internal  string                                  // 小文字始まり → 出力されない
}

func main() {
    full := Item{ID: 1, Name: "ノート", Note: "A5", Tags: []string{"文具"}, Stock: 5, Price: 300,
        CreatedAt: time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC), Code: 9007199254740993,
        Secret: "xxx", Dash: "d", NoTag: "n", internal: "i"}
    empty := Item{ID: 2, Name: "ペン", Tags: []string{}}

    for _, v := range []Item{full, empty} {
        b, err := json.MarshalIndent(v, "", "  ")
        if err != nil {
            panic(err)
        }
        fmt.Println(string(b))
    }
}

実行結果(Go 1.27.1):

{
  "id": 1,
  "name": "ノート",
  "note": "A5",
  "tags": [
    "文具"
  ],
  "stock": 5,
  "price": 300,
  "created_at": "2026-10-01T09:00:00Z",
  "updated_at": "0001-01-01T00:00:00Z",
  "code": "9007199254740993",
  "-": "d",
  "NoTag": "n"
}
{
  "id": 2,
  "name": "ペン",
  "updated_at": "0001-01-01T00:00:00Z",
  "code": "0",
  "-": "",
  "NoTag": ""
}

出力から分かることを整理します。

フィールド 値が空のときの出力 理由
note,omitempty 省略 空文字は「空」
tags,omitempty 省略 空スライス []string{} も nil と同じく省略
stock,omitempty 省略 v1 では 0 も「空」
price,omitzero 省略 ゼロ値
created_at,omitzero 省略 time.Time の IsZero() が true
updated_at,omitempty "0001-01-01T00:00:00Z" が出る 構造体は omitempty の対象外
code,string "0" 文字列として出る
Secret(-) 出ない 常に除外
Dash(-,) "-": "" キー名が -
internal 出ない 小文字始まり(unexported)

日時(time.Time)を「未設定なら省略」したいときは、omitempty ではなく omitzero を使います。 omitempty では 0001-01-01T00:00:00Z が出力されてしまいます。また code,string のように 64 ビット整数を文字列で出すと、JavaScript など 2 の 53 乗を超える整数を正確に扱えない受け手にも値を正しく渡せます。

omitemptyとomitzeroの使い分け

型 omitempty(v1) omitzero おすすめ
string "" で省略 "" で省略 どちらでも
int・float・bool 0・false で省略 0・false で省略 omitzero(v2 でも同じ意味)
スライス・map nil と空の両方で省略 nil のときだけ省略 空も省きたいなら omitempty
ポインタ nil で省略 nil で省略 どちらでも
構造体(time.Time など) 省略されない ゼロ値・IsZero() で省略 omitzero

v2 では omitempty の意味が「JSON にしたときに null・空文字・空オブジェクト・空配列になるもの」に変わり、数値の 0 や false は omitempty で省略されなくなります(後半の v1/v2 比較で実行結果を示します)。数値や bool に omitempty を使っている場合は、omitzero に置き換えておくと v1 と v2 で同じ結果になります。

Marshal・Unmarshal・Encoder・Decoderのコピペコード

よく使う10パターンを1ファイルにまとめました。go mod init example.com/jsondemo したフォルダに main.go として保存し、go run . で動きます。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "time"
)

type Order struct {
    ID      int       `json:"id"`
    Member  string    `json:"member"`
    Items   []string  `json:"items"`
    OrderAt time.Time `json:"order_at"`
}

// カスタム型:JSON では "low" / "high" の文字列で表す
type Priority int

const (
    Low Priority = iota
    High
)

func (p Priority) MarshalJSON() ([]byte, error) {
    s := map[Priority]string{Low: "low", High: "high"}[p]
    return json.Marshal(s)
}

func (p *Priority) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    switch s {
    case "low":
        *p = Low
    case "high":
        *p = High
    default:
        return fmt.Errorf("priority が不正です: %q", s)
    }
    return nil
}

type Task struct {
    Title    string   `json:"title"`
    Priority Priority `json:"priority"`
}

// 種類によって中身が違う JSON は RawMessage で後回しにする
type Event struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"`
}

func main() {
    // 1) Marshal:構造体 → JSON
    o := Order{ID: 1, Member: "佐藤", Items: []string{"ノート", "ペン"},
        OrderAt: time.Date(2026, 10, 1, 9, 30, 0, 0, time.FixedZone("JST", 9*3600))}
    b, _ := json.Marshal(o)
    fmt.Println("1) Marshal:", string(b))

    // 2) Unmarshal:JSON → 構造体
    var o2 Order
    err := json.Unmarshal([]byte(`{"id":2,"member":"鈴木","items":["付箋"],"order_at":"2026-10-01T10:00:00+09:00"}`), &o2)
    fmt.Printf("2) Unmarshal: %+v err=%v\n", o2, err)

    // 3) map[string]any:数値は float64 になる
    var m map[string]any
    json.Unmarshal([]byte(`{"id":3,"price":1200,"tags":["文具"],"ok":true,"memo":null}`), &m)
    for _, k := range []string{"id", "price", "tags", "ok", "memo"} {
        fmt.Printf("3) m[%q] = %v (%T)\n", k, m[k], m[k])
    }

    // 4) UseNumber:数値を json.Number(文字列)のまま受け取る
    dec := json.NewDecoder(strings.NewReader(`{"id":9007199254740993}`))
    dec.UseNumber()
    var mn map[string]any
    dec.Decode(&mn)
    n := mn["id"].(json.Number)
    i64, _ := n.Int64()
    fmt.Printf("4) Number: %v (%T) Int64=%d / float64だと %.0f\n", n, n, i64, m2f(`{"id":9007199254740993}`))

    // 5) Encoder:io.Writer に直接書く(末尾に改行がつく)
    enc := json.NewEncoder(os.Stdout)
    enc.SetIndent("", "  ")
    fmt.Print("5) Encoder:\n")
    enc.Encode(map[string]int{"ノート": 5, "ペン": 0})

    // 6) Decoder:複数の JSON を順に読む(JSON Lines)
    lines := `{"title":"在庫確認","priority":"high"}
{"title":"備品発注","priority":"low"}`
    d := json.NewDecoder(strings.NewReader(lines))
    for d.More() {
        var t Task
        if err := d.Decode(&t); err != nil {
            fmt.Println("6) error:", err)
            break
        }
        fmt.Printf("6) Decoder: %+v\n", t)
    }

    // 7) カスタム MarshalJSON / UnmarshalJSON
    tb, _ := json.Marshal(Task{Title: "棚卸し", Priority: High})
    fmt.Println("7) MarshalJSON:", string(tb))
    var bad Task
    fmt.Println("7) UnmarshalJSON:", json.Unmarshal([]byte(`{"title":"x","priority":"urgent"}`), &bad))

    // 8) RawMessage:type を見てから data を解釈する
    var ev Event
    json.Unmarshal([]byte(`{"type":"task","data":{"title":"清掃","priority":"low"}}`), &ev)
    fmt.Printf("8) RawMessage: type=%s data=%s\n", ev.Type, ev.Data)
    if ev.Type == "task" {
        var t Task
        json.Unmarshal(ev.Data, &t)
        fmt.Printf("   → %+v\n", t)
    }

    // 9) DisallowUnknownFields:知らないキーをエラーにする
    d2 := json.NewDecoder(strings.NewReader(`{"title":"点検","priority":"low","owner":"田中"}`))
    d2.DisallowUnknownFields()
    var t2 Task
    fmt.Println("9) DisallowUnknownFields:", d2.Decode(&t2))

    // 10) Indent / Valid
    var buf bytes.Buffer
    json.Indent(&buf, []byte(`{"a":1,"b":[1,2]}`), "", " ")
    fmt.Println("10) Indent:", strings.ReplaceAll(buf.String(), "\n", "⏎"))
    fmt.Println("10) Valid:", json.Valid([]byte(`{"a":1}`)), json.Valid([]byte(`{a:1}`)))
}

func m2f(s string) float64 {
    var m map[string]any
    json.Unmarshal([]byte(s), &m)
    return m["id"].(float64)
}

実行結果(Go 1.27.1):

1) Marshal: {"id":1,"member":"佐藤","items":["ノート","ペン"],"order_at":"2026-10-01T09:30:00+09:00"}
2) Unmarshal: {ID:2 Member:鈴木 Items:[付箋] OrderAt:2026-10-01 10:00:00 +0900 JST} err=<nil>
3) m["id"] = 3 (float64)
3) m["price"] = 1200 (float64)
3) m["tags"] = [文具] ([]interface {})
3) m["ok"] = true (bool)
3) m["memo"] = <nil> (<nil>)
4) Number: 9007199254740993 (json.Number) Int64=9007199254740993 / float64だと 9007199254740992
5) Encoder:
{
  "ノート": 5,
  "ペン": 0
}
6) Decoder: {Title:在庫確認 Priority:1}
6) Decoder: {Title:備品発注 Priority:0}
7) MarshalJSON: {"title":"棚卸し","priority":"high"}
7) UnmarshalJSON: priority が不正です: "urgent"
8) RawMessage: type=task data={"title":"清掃","priority":"low"}
   → {Title:清掃 Priority:0}
9) DisallowUnknownFields: json: unknown field "owner"
10) Indent: {⏎ "a": 1,⏎ "b": [⏎  1,⏎  2⏎ ]⏎}
10) Valid: true false

※ 2) の JST は、実行環境のタイムゾーンが Asia/Tokyo のため表示されています。

結果のポイント

番号 読み取れること
1・2 time.Time は RFC 3339 形式(2026-10-01T09:30:00+09:00)で読み書きされる。 タイムゾーンのオフセットも保たれる
3 map[string]any に入れると、JSON の数値はすべて float64、配列は []any、null は nil になる。整数として使うときは int(m["id"].(float64)) のように変換が必要
4 9007199254740993 は float64 だと ...992 に丸められる。ID など大きな整数は UseNumber() で json.Number として受けると正確
5 Encoder は1件ごとに末尾に改行を付ける。map のキーは v1 ではソートされて出力される
6 Decoder と More() で、改行区切りの JSON(JSON Lines)を1件ずつ読める。ファイルやHTTPレスポンスを全部メモリに載せずに処理できる
7 MarshalJSON は値レシーバ、UnmarshalJSON はポインタレシーバで定義するのが定番。UnmarshalJSON で返したエラーは Unmarshal のエラーとして呼び出し元に届く
8 RawMessage は JSON をそのまま []byte で保持する型。type を見てから中身の構造体を決める、という二段階の読み込みに使う
9 既定では知らないキーは無視される。設定ファイルの書き間違いを見つけたいときは DisallowUnknownFields を付ける

MarshalとEncoder、UnmarshalとDecoderの使い分け

状況 使うもの 理由
小さな JSON を変数で扱う Marshal / Unmarshal []byte でやり取りでき、手軽
HTTP レスポンスに書く json.NewEncoder(w).Encode(v) http.ResponseWriter に直接書ける
HTTP リクエストのボディを読む json.NewDecoder(r.Body).Decode(&v) 全体を読み込んでから変換する手間がない
大きなファイル・JSON Lines Decoder と More() 1件ずつ処理できる
知らないキーをエラーにしたい Decoder.DisallowUnknownFields() Unmarshal にはこの設定がない(v2 は RejectUnknownMembers)

encoding/json v1とv2の挙動差を実行して比べる

同じ構造体を v1 と v2 で変換し、結果の違いを確かめます。v1 と v2 を同じファイルで使うため、import に別名を付けています。

package main

import (
    jsonv1 "encoding/json"
    "encoding/json/jsontext"
    json "encoding/json/v2"
    "fmt"
    "time"
)

type Item struct {
    Name  string            `json:"name"`
    Tags  []string          `json:"tags"`
    Attrs map[string]string `json:"attrs"`
    Stock int               `json:"stock,omitempty"`
}

type Member struct {
    UserName string `json:"user_name"`
}

func show(label string, b []byte, err error) {
    fmt.Printf("%-28s %s err=%v\n", label, b, err)
}

func main() {
    // 1) nil スライス・nil map
    it := Item{Name: "ノート"}
    b1, e1 := jsonv1.Marshal(it)
    b2, e2 := json.Marshal(it)
    show("1) nil slice/map v1:", b1, e1)
    show("1) nil slice/map v2:", b2, e2)
    b3, e3 := json.Marshal(it, json.FormatNilSliceAsNull(true), json.FormatNilMapAsNull(true))
    show("1) v2+FormatNil...AsNull:", b3, e3)

    // 2) 重複キー
    dup := []byte(`{"name":"ノート","name":"ペン"}`)
    var a, b Item
    fmt.Println("2) 重複キー v1:", jsonv1.Unmarshal(dup, &a), a.Name)
    fmt.Println("2) 重複キー v2:", json.Unmarshal(dup, &b))
    var c Item
    fmt.Println("2) v2+AllowDuplicateNames:", json.Unmarshal(dup, &c, jsontext.AllowDuplicateNames(true)), c.Name)

    // 3) 大文字小文字
    in := []byte(`{"USER_NAME":"佐藤"}`)
    var m1, m2, m3 Member
    jsonv1.Unmarshal(in, &m1)
    json.Unmarshal(in, &m2)
    json.Unmarshal(in, &m3, json.MatchCaseInsensitiveNames(true))
    fmt.Printf("3) 大文字小文字 v1=%q v2=%q v2+MatchCaseInsensitiveNames=%q\n", m1.UserName, m2.UserName, m3.UserName)

    // 4) 不正な UTF-8
    bad := string([]byte{'A', 0xff, 'B'})
    b1, e1 = jsonv1.Marshal(bad)
    b2, e2 = json.Marshal(bad)
    show("4) 不正UTF-8 v1:", b1, e1)
    show("4) 不正UTF-8 v2:", b2, e2)

    // 5) omitempty の意味の違い(0 は v2 では省略されない)
    b1, _ = jsonv1.Marshal(Item{Name: "ペン", Tags: []string{}, Attrs: map[string]string{}})
    b2, _ = json.Marshal(Item{Name: "ペン", Tags: []string{}, Attrs: map[string]string{}})
    show("5) stock=0 omitempty v1:", b1, nil)
    show("5) stock=0 omitempty v2:", b2, nil)

    // 6) time.Duration
    type Job struct {
        Timeout time.Duration `json:"timeout"`
    }
    b1, e1 = jsonv1.Marshal(Job{3 * time.Second})
    b2, e2 = json.Marshal(Job{3 * time.Second})
    show("6) Duration v1:", b1, e1)
    show("6) Duration v2:", b2, e2)

    // 7) 知らないキー
    var k Member
    fmt.Println("7) 未知のキー v2既定:", json.Unmarshal([]byte(`{"user_name":"a","age":1}`), &k))
    fmt.Println("7) v2+RejectUnknownMembers:", json.Unmarshal([]byte(`{"user_name":"a","age":1}`), &k, json.RejectUnknownMembers(true)))

    // 8) HTML 文字のエスケープ有無(v1 はエスケープ、v2 はそのまま)
    b1, _ = jsonv1.Marshal("A&B")
    b2, _ = json.Marshal("A&B")
    fmt.Printf("8) \"A&B\" v1=%d bytes v2=%d bytes v2=%s\n", len(b1), len(b2), b2)

    // 9) v2 で整形出力
    b2, _ = json.Marshal(Member{"佐藤"}, jsontext.WithIndent("  "))
    fmt.Printf("9) WithIndent:\n%s\n", b2)
}

実行結果(Go 1.27.1):

1) nil slice/map v1:         {"name":"ノート","tags":null,"attrs":null} err=<nil>
1) nil slice/map v2:         {"name":"ノート","tags":[],"attrs":{},"stock":0} err=<nil>
1) v2+FormatNil...AsNull:    {"name":"ノート","tags":null,"attrs":null,"stock":0} err=<nil>
2) 重複キー v1: <nil> ペン
2) 重複キー v2: jsontext: duplicate object member name "name"
2) v2+AllowDuplicateNames: <nil> ペン
3) 大文字小文字 v1="佐藤" v2="" v2+MatchCaseInsensitiveNames="佐藤"
4) 不正UTF-8 v1:               "A�B" err=<nil>
4) 不正UTF-8 v2:                err=jsontext: invalid UTF-8
5) stock=0 omitempty v1:     {"name":"ペン","tags":[],"attrs":{}} err=<nil>
5) stock=0 omitempty v2:     {"name":"ペン","tags":[],"attrs":{},"stock":0} err=<nil>
6) Duration v1:              {"timeout":3000000000} err=<nil>
6) Duration v2:              {"timeout" err=json: cannot marshal from Go time.Duration within "/timeout": no default representation
7) 未知のキー v2既定: <nil>
7) v2+RejectUnknownMembers: json: cannot unmarshal JSON string into Go main.Member: unknown object member name "age"
8) "A&B" v1=10 bytes v2=5 bytes v2="A&B"
9) WithIndent:
{
  "user_name": "佐藤"
}

v1とv2の違い一覧(実行結果まとめ)

項目 v1(encoding/json) v2(encoding/json/v2) v2 で v1 と同じにするオプション
nil スライス・nil map null []・{} json.FormatNilSliceAsNull(true)・json.FormatNilMapAsNull(true)
重複したキー 後の値で上書き(エラーなし) エラー duplicate object member name jsontext.AllowDuplicateNames(true)
キーの大文字小文字 区別しない(USER_NAME も読める) 区別する(読めず空のまま) json.MatchCaseInsensitiveNames(true) またはタグ case:ignore
不正な UTF-8 置換文字(�)に置き換えて成功 エラー invalid UTF-8 jsontext.AllowInvalidUTF8(true)
omitempty と数値の 0 省略 出力される タグを omitzero に変更
time.Duration ナノ秒の数値 エラー(既定の表現なし) json.FormatDurationAsNano(true)(v1 側のオプション)
未知のキー 無視 無視 エラーにするなら json.RejectUnknownMembers(true)
< > & Unicode エスケープする(A&B が10バイト) そのまま(5バイト) jsontext.EscapeForHTML(true)
map のキー順 ソートされる 順不同 json.Deterministic(true)

v2 は「受け取った JSON の曖昧さを見逃さない」方向の既定値です。 重複キーや大文字小文字違いのキーは、送り手と受け手で解釈が分かれる原因になるため、v2 ではエラーや不一致として表に出ます。v1 の動きのまま v2 の API を使いたい場合は、上の表のオプションを付けます。

6) の Duration の行のとおり、エラーのときは戻り値の []byte に途中までの内容({"timeout")が入っていることがあります。 err が nil でないときは戻り値を使わないようにしましょう。

v2だけのタグ:case と embed

package main

import (
    "encoding/json/jsontext"
    json "encoding/json/v2"
    "fmt"
)

type Audit struct {
    CreatedBy string `json:"created_by"`
    UpdatedBy string `json:"updated_by"`
}

type Article struct {
    ID    int                       `json:"id"`
    Title string                    `json:"title,case:ignore"` // このフィールドだけ大文字小文字を無視
    Views int64                     `json:"views,string"`      // 数値を文字列で
    Audit Audit                     `json:",embed"`            // 中身を親に展開
    Extra map[string]jsontext.Value `json:",embed"`            // 知らないキーを受け止める
}

type BadString struct {
    Name string `json:"name,string"` // v2 では数値以外に string は使えない
}

type Hidden struct {
    secret string
}

func main() {
    in := []byte(`{"id":1,"TITLE":"在庫の数え方","views":"120","created_by":"佐藤","updated_by":"鈴木","lang":"ja","draft":true}`)
    var a Article
    err := json.Unmarshal(in, &a)
    fmt.Printf("Unmarshal: %+v err=%v\n", a, err)
    out, err := json.Marshal(a, json.Deterministic(true))
    fmt.Printf("Marshal:   %s err=%v\n", out, err)

    _, err = json.Marshal(BadString{"x"})
    fmt.Println("string を文字列に:", err)

    _, err = json.Marshal(Hidden{"x"})
    fmt.Println("unexported のみの構造体:", err)
}

実行結果(Go 1.27.1):

Unmarshal: {ID:1 Title:在庫の数え方 Views:120 Audit:{CreatedBy:佐藤 UpdatedBy:鈴木} Extra:map[draft:true lang:"ja"]} err=<nil>
Marshal:   {"id":1,"title":"在庫の数え方","views":"120","created_by":"佐藤","updated_by":"鈴木","draft":true,"lang":"ja"} err=<nil>
string を文字列に: json: unable to marshal from Go string within "/name": invalid use of `string` tag option
unexported のみの構造体: json: unable to marshal from Go main.Hidden: Go struct has no exported fields
タグ 動き
case:ignore TITLE のように大文字のキーでも Title に入る(フィールド単位で指定できる)
embed(構造体) created_by・updated_by が入れ子にならず、親と同じ階層に並ぶ
embed(map[string]jsontext.Value) 構造体にないキー(lang・draft)を受け止め、Marshal で元に戻せる。未知のキーを落とさずに中継したい場合に便利
string(v2) 数値以外に付けるとエラー。v1 では文字列や bool にも使えた

また v2 では、エクスポートされたフィールドが1つもない構造体はエラーになります(v1 では {} が出力されます)。

よくあるエラーと直し方

package main

import (
    "encoding/json"
    "errors"
    "fmt"
)

type Member struct {
    name string `json:"name"` // 小文字始まり(unexported)
    Age  int    `json:"age"`
}

type Stock struct {
    ID    int `json:"id"`
    Count int `json:"count"`
}

func main() {
    // 1) 小文字始まりのフィールドは出力も読み込みもされない
    b, err := json.Marshal(Member{name: "佐藤", Age: 30})
    fmt.Println("1)", string(b), err)

    // 2) 型が合わない
    var s Stock
    err = json.Unmarshal([]byte(`{"id":1,"count":"10"}`), &s)
    fmt.Printf("2) %v\n   → %+v\n", err, s)
    var te *json.UnmarshalTypeError
    if errors.As(err, &te) {
        fmt.Printf("   UnmarshalTypeError: Field=%s Value=%s Type=%v Offset=%d\n", te.Field, te.Value, te.Type, te.Offset)
    }

    // 3) JSON の文法エラー
    err = json.Unmarshal([]byte(`{"id":1,}`), &s)
    fmt.Println("3)", err)
    var se *json.SyntaxError
    if errors.As(err, &se) {
        fmt.Println("   SyntaxError Offset:", se.Offset)
    }

    // 4) ポインタを渡し忘れ
    err = json.Unmarshal([]byte(`{"id":1}`), s)
    fmt.Println("4)", err)

    // 5) 数値の範囲外
    var small struct{ N int8 `json:"n"` }
    fmt.Println("5)", json.Unmarshal([]byte(`{"n":300}`), &small))

    // 6) 出力できない型
    _, err = json.Marshal(map[string]any{"f": func() {}})
    fmt.Println("6)", err)
    _, err = json.Marshal(map[string]any{"ch": make(chan int)})
    fmt.Println("6)", err)
}

実行結果(Go 1.27.1):

1) {"age":30} <nil>
2) json: cannot unmarshal string into Go struct field Stock.count of type int
   → {ID:1 Count:0}
   UnmarshalTypeError: Field=count Value=string Type=int Offset=20
3) invalid character '}' looking for beginning of object key string
   SyntaxError Offset: 9
4) json: Unmarshal(non-pointer main.Stock)
5) json: cannot unmarshal number 300 into Go struct field .n of type int8
6) json: unsupported type: func()
6) json: unsupported type: chan int

このコードに go vet をかけると、1) と 4) は実行前に指摘されます。

$ go vet ./errs
errs/main.go:10:2: struct field name has json tag but is not exported
errs/main.go:42:22: call of Unmarshal passes non-pointer as second argument
エラー・症状 原因 直し方
フィールドが出力されない({"age":30} だけ) 小文字始まりのフィールドは encoding/json から見えない フィールド名を大文字始まり(Name)にし、キー名はタグ json:"name" で小文字に
cannot unmarshal string into Go struct field Stock.count of type int JSON は "10"(文字列)、Go は int 送り手を数値にする/Go 側にタグ json:"count,string" を付ける
invalid character '}' looking for beginning of object key string 末尾のカンマなど JSON の文法の誤り json.Valid で事前に確認、SyntaxError.Offset で位置を特定
json: Unmarshal(non-pointer main.Stock) Unmarshal にポインタでない値を渡した &s を渡す
cannot unmarshal number 300 ... of type int8 型の範囲を超えた 型を大きくする(int・int64)
json: unsupported type: func() 関数・チャネルは JSON にできない json:"-" で除外する
数値の型が float64 で型アサーションに失敗 map[string]any の数値は float64 .(float64) で受ける、または構造体に定義

Unmarshal はエラーが起きても、それまでに読めたフィールドは埋めています(2) の ID:1)。エラーのときは構造体の中身を使わずに処理を中断するのが安全です。

チェックリスト

確認すること 方法
フィールドは大文字始まりか go vet で警告を確認
Unmarshal にポインタを渡しているか &v になっているか、go vet で確認
日時の省略に omitempty を使っていないか time.Time には omitzero
数値・bool に omitempty を使っていないか v2 でも同じ結果にするなら omitzero
大きな整数 ID を float64 で受けていないか 構造体の int64、または UseNumber
外部からの JSON を厳密に検証したいか DisallowUnknownFields(v1)/RejectUnknownMembers(v2)
エラー時に戻り値を使っていないか if err != nil { return err } を先に書く

よくある質問

Q. encoding/json から encoding/json/v2 に移行する必要はありますか? A. 必須ではありません。Go 1.27 のリリースノートでも、v1 の API は今後もサポートされ、移行は求められていないと明記されています。しかも v1 は内部が v2 の実装になったため、Unmarshal の高速化はそのまま受けられます。新しく書くコードで、重複キーや大文字小文字違いを厳密に扱いたい場合に v2 を選ぶとよいでしょう。

Q. omitemptyで0やfalseが省略されて困ります。 A. 0 や false も意味のある値として出したい場合は、ポインタ型(*int)にして nil のときだけ省略するか、タグから omitempty を外します。逆に「未設定なら省略」したいときは omitzero を使うと v1・v2 で同じ動きになります。

Q. JSONのキーがスネークケース(user_name)でもタグは必要ですか? A. 必要です。Go のフィールド名は大文字始まり(UserName)なので、そのままでは UserName というキーで出力されます。json:"user_name" のようにタグで指定します。 v1 は読み込み時に大文字小文字を区別しませんが、アンダースコアの有無は区別するため、user_name と UserName は一致しません。

Q. 形の分からないJSONを読むにはどうしますか? A. map[string]any に Unmarshal し、型アサーションで取り出します。一部だけ形が決まっている場合は、決まっている部分を構造体に、可変部分を json.RawMessage にして後から解釈すると、型アサーションを減らせます。

まとめ

  • Go の JSON は encoding/json と構造体タグで扱う。キー名・omitempty・omitzero・string・- が基本のタグ
  • time.Time の省略は omitzero、数値・bool も omitzero にすると v1 と v2 で結果がそろう
  • Go 1.27 で encoding/json/v2 と jsontext が正式に使えるようになり、v1 も内部が v2 実装に。v1 の API はそのまま使える
  • v2 は nil スライスを []、重複キー・不正 UTF-8 をエラー、大文字小文字を区別。オプションで v1 の動きにできる
  • 大きな整数は UseNumber や int64 で受ける。map[string]any の数値は float64
  • フィールドの小文字始まり・ポインタの渡し忘れは go vet で事前に見つかる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Go JSON|構造体タグ一覧とencoding/json v2の変更点」

株式会社RJCでは、GoでのJSON処理を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?