JSON 形状を合成する serde(flatten)
#[serde(flatten)] を使って埋め込み構造体のフィールドを親 JSON オブジェクトにインライン化します。共通エンベロープのコード再利用がきれいになります。
詳細な説明
埋め込み構造体を親にフラット化する
#[serde(flatten)] は、埋め込み構造体のフィールドをキー配下にネストするのではなく、親の JSON オブジェクトにマージするよう serde に指示する属性です。多数の DTO で共通する「エンベロープ」フィールドを共有する標準的な方法です。
課題
すべての API レスポンスが共通の request_id と timestamp を持つとします。
{
"request_id": "abc123",
"timestamp": "2024-06-15T10:00:00Z",
"user": { "id": 1, "name": "Ada" }
}
flatten がなければ、request_id と timestamp をすべてのレスポンス構造体で繰り返し記述する必要があり、面倒で間違いの元です。
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_id、timestamp、user がトップレベルに並ぶ形(仕様通り)になり、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 が活躍します。