スパース出力のための skip_serializing_if
#[serde(skip_serializing_if = "Option::is_none")] を使って None フィールドを生成 JSON から省略します。PATCH リクエストの定石パターンです。
詳細な説明
None を JSON 出力から省略する
デフォルトでは、None の値を持つ Option<T> フィールドは "field": null としてシリアライズされます。PATCH リクエストやスパースなレスポンスでは、フィールドそのものを消したいことが多いはずです。#[serde(skip_serializing_if = "Option::is_none")] がまさにそれを実現します。
構造体の例
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUser {
pub id: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age: Option<i32>,
}
動作
let patch = UpdateUser {
id: 1,
name: Some("Ada".into()),
email: None,
age: None,
};
let body = serde_json::to_string(&patch)?;
// {"id":1,"name":"Ada"}
email と age は出力に含まれません。サーバ側はその不在を「変更しない」と解釈し、「null をセット」とは解釈しません。
なぜこれが PATCH の定石か
REST PATCH の意味論では、各フィールドに対して 3 つの状態を区別できます。
- フィールド不在 → 既存値を変更しない。
- フィールド+値あり → 新しい値で更新する。
- フィールド+null → カラムを NULL にする。
skip_serializing_if がないと状態 1 を表現できず、すべての None が状態 3 になり、意図せずデータが消えてしまいます。
カスタム skip 関数
Option::is_none が最も一般的ですが、bool を返す任意の関数を指定できます。
fn is_zero(n: &i32) -> bool { *n == 0 }
#[serde(skip_serializing_if = "is_zero")]
pub retries: i32,
これで retries がゼロのとき出力から省略され、デフォルト値で十分な任意設定キーをオプトアウトできます。
default と組み合わせて完全往復に
#[serde(default)] も併用すれば、デシリアライズ時にキーが欠けていても受け入れられます。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
これで JSON にフィールドが含まれていてもいなくても、構造体はクリーンに往復できます。
ユースケース
REST API の PATCH エンドポイント、スパースなミューテーションペイロード、「未指定」と「クリア」を区別する必要があるケースに最適です。Rust + serde の部分更新ペイロード作成における正攻法と言えるパターンです。