中学理科1631340 views
高校日本史190647 views
高校化学2926093 views
教育149595 views
いろは3014035 views
ヒストリア291495 views
高校国語788661 views
りんご212052 views
高校倫理1441073 views
小学理科720260 views
Help
Tools

English

Go で JSON のカスタムエンコード・デコードを実装する:MarshalJSON と UnmarshalJSON

encoding/json のデフォルト挙動では対応しきれない場面があります。列挙型を文字列で出力したい場合、特殊なフォーマットの値を扱いたい場合などです。MarshalJSONUnmarshalJSON を実装すれば、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
}

エイリアスで TypeTimestamp を通常どおりパースし、その後に Timestamptime.Time に変換して OccuredAt にセットしています。

フィールド単位のカスタマイズ

特定の型(Status や日付など)だけ変換を変えたい場合に使う。影響範囲が限定的で安全。

構造体全体のカスタマイズ

フィールドの追加・除外や複合的なデータ変換が必要な場合に使う。エイリアストリック必須。

インターフェースの確認

MarshalJSONUnmarshalJSON は、それぞれ json.Marshalerjson.Unmarshaler インターフェースに対応しています。

type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
	UnmarshalJSON([]byte) error
}

メソッドのシグネチャを間違えると、コンパイルは通るのにカスタム処理が呼ばれないという厄介なバグになります。以下のようなコンパイル時チェックを入れておくと安心です。

var _ json.Marshaler = (*Status)(nil)
var _ json.Unmarshaler = (*Status)(nil)

Status がインターフェースを満たしていなければコンパイルエラーになるため、シグネチャのミスを早期に検出できます。ライブラリを開発する場合には特に有用なテクニックです。