JSON 形状を合成する serde(flatten)

#[serde(flatten)] を使って埋め込み構造体のフィールドを親 JSON オブジェクトにインライン化します。共通エンベロープのコード再利用がきれいになります。

Patterns

詳細な説明

埋め込み構造体を親にフラット化する

#[serde(flatten)] は、埋め込み構造体のフィールドをキー配下にネストするのではなく、親の JSON オブジェクトにマージするよう serde に指示する属性です。多数の DTO で共通する「エンベロープ」フィールドを共有する標準的な方法です。

課題

すべての API レスポンスが共通の request_idtimestamp を持つとします。

{
  "request_id": "abc123",
  "timestamp": "2024-06-15T10:00:00Z",
  "user": { "id": 1, "name": "Ada" }
}

flatten がなければ、request_idtimestamp をすべてのレスポンス構造体で繰り返し記述する必要があり、面倒で間違いの元です。

flatten を使う

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
    pub request_id: String,
    pub timestamp: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserResponse {
    #[serde(flatten)]
    pub envelope: Envelope,
    pub user: User,
}

wire 上の JSON は request_idtimestampuser がトップレベルに並ぶ形(仕様通り)になり、Rust 側からは response.envelope.request_id でアクセスできます。

catch-all マップ

末尾の HashMap フィールドに flatten を付けて、未知のキーをまとめて吸い上げることもできます。

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub event_type: String,

    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

これで構造体に明示されていないキーは extra に格納され、上流サービスが新しいフィールドを追加しても前方互換性が保たれます。

注意点

  • flatten は親の deny_unknown_fields と併用できません。catch-all 動作と厳格検証が衝突するためです。
  • ネスト構造体より若干遅くなります。serde が二段階デシリアライズを行ってフィールドをバッファし、再ルーティングするためです。
  • 数値タグフィールドは確実にフラット化できません。マージしたキー名がユニークである必要があります。

採用すべきとき

複数の DTO が共通のヘッダ、フッタ、メタデータブロックを持つ場合に flatten を選んでください。wire 形式のネスト構造を歪めずに Rust 型を DRY に保てます。

ユースケース

API レスポンスのエンベロープ、JSON:API 仕様のボディ、GraphQL の extensions、イベントメタデータブロックなど、共通フィールドの合成が必要な場面で flatten が活躍します。

試してみる — JSON to Rust Struct Converter

フルツールを開く