- Go言語
Go JSON|構造体タグ一覧とencoding/json v2の変更点
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期連続の黒字決算。