Go で JSON のカスタムエンコード・デコードを実装する:MarshalJSON と UnmarshalJSON
encoding/json のデフォルト挙動では対応しきれない場面があります。列挙型を文字列で出力したい場合、特殊なフォーマットの値を扱いたい場合などです。MarshalJSON と UnmarshalJSON を実装すれば、JSON の変換ロジックを完全にコントロールできます。
MarshalJSON でエンコードを変える
型に MarshalJSON() ([]byte, error) メソッドを実装すると、json.Marshal 時にそのメソッドが呼ばれます。
type Status int
const (
StatusActive Status = 1
StatusInactive Status = 2
StatusBanned Status = 3
)
func (s Status) MarshalJSON() ([]byte, error) {
var str string
switch s {
case StatusActive:
str = "active"
case StatusInactive:
str = "inactive"
case StatusBanned:
str = "banned"
default:
str = "unknown"
}
return json.Marshal(str)
}この Status 型を含む構造体を Marshal すると、数値ではなく文字列が出力されます。
type User struct {
Name string `json:"name"`
Status Status `json:"status"`
}
func main() {
user := User{Name: "Alice", Status: StatusActive}
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"name":"Alice","status":"active"}
}API のレスポンスで列挙値を文字列にするのは一般的なプラクティスです。クライアントが数値の意味を別途調べる必要がなくなるため、API の使いやすさが向上します。
UnmarshalJSON でデコードを変える
逆方向のカスタマイズが UnmarshalJSON(data []byte) error です。JSON の値を受け取って Go の値に変換するロジックを自分で書けます。
func (s *Status) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
switch str {
case "active":
*s = StatusActive
case "inactive":
*s = StatusInactive
case "banned":
*s = StatusBanned
default:
return fmt.Errorf("不明なステータス: %s", str)
}
return nil
}"active" という文字列が StatusActive(1)に変換されます。未知の値が来た場合にエラーを返せるので、不正データの検出にも役立ちます。
JSON 文字列 “active” を受信
UnmarshalJSON が呼ばれる
switch で定数に変換
構造体フィールドに格納
エイリアストリックで無限ループを防ぐ
構造体全体の MarshalJSON を実装するとき、最大の注意点は無限再帰です。
// 危険なコード(無限ループ)
func (p Product) MarshalJSON() ([]byte, error) {
return json.Marshal(p) // 自分自身の MarshalJSON が再び呼ばれる
}この問題はエイリアス型を使って回避します。エイリアス型には元の型のメソッドが引き継がれないため、デフォルトのエンコードが使われます。
type Product struct {
Name string `json:"name"`
Price int `json:"price"`
}
func (p Product) MarshalJSON() ([]byte, error) {
type Alias Product
return json.Marshal(struct {
Alias
PriceText string `json:"price_text"`
}{
Alias: Alias(p),
PriceText: fmt.Sprintf("¥%d", p.Price),
})
}product := Product{Name: "ノート", Price: 500}
data, _ := json.Marshal(product)
fmt.Println(string(data))
// {"name":"ノート","price":500,"price_text":"¥500"}元のフィールドに加えて price_text を動的に追加できました。エイリアスを経由することでデフォルトの Marshal が使われ、無限ループにはなりません。
UnmarshalJSON でもエイリアスを使う
デコード側でも同じパターンが有効です。通常のフィールドはデフォルト処理に任せ、追加の変換だけ自分で書けます。
type Event struct {
Type string `json:"type"`
Timestamp int64 `json:"timestamp"`
OccuredAt time.Time
}
func (e *Event) UnmarshalJSON(data []byte) error {
type Alias Event
aux := &struct {
*Alias
}{
Alias: (*Alias)(e),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
e.OccuredAt = time.Unix(e.Timestamp, 0)
return nil
}エイリアスで Type と Timestamp を通常どおりパースし、その後に Timestamp を time.Time に変換して OccuredAt にセットしています。
特定の型(Status や日付など)だけ変換を変えたい場合に使う。影響範囲が限定的で安全。
フィールドの追加・除外や複合的なデータ変換が必要な場合に使う。エイリアストリック必須。
インターフェースの確認
MarshalJSON と UnmarshalJSON は、それぞれ json.Marshaler と json.Unmarshaler インターフェースに対応しています。
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}メソッドのシグネチャを間違えると、コンパイルは通るのにカスタム処理が呼ばれないという厄介なバグになります。以下のようなコンパイル時チェックを入れておくと安心です。
var _ json.Marshaler = (*Status)(nil)
var _ json.Unmarshaler = (*Status)(nil)Status がインターフェースを満たしていなければコンパイルエラーになるため、シグネチャのミスを早期に検出できます。ライブラリを開発する場合には特に有用なテクニックです。











