スパース出力のための skip_serializing_if

#[serde(skip_serializing_if = "Option::is_none")] を使って None フィールドを生成 JSON から省略します。PATCH リクエストの定石パターンです。

Patterns

詳細な説明

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"}

emailage は出力に含まれません。サーバ側はその不在を「変更しない」と解釈し、「null をセット」とは解釈しません。

なぜこれが PATCH の定石か

REST PATCH の意味論では、各フィールドに対して 3 つの状態を区別できます。

  1. フィールド不在 → 既存値を変更しない。
  2. フィールド+値あり → 新しい値で更新する。
  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 の部分更新ペイロード作成における正攻法と言えるパターンです。

試してみる — JSON to Rust Struct Converter

フルツールを開く