LoginSignup
1
0

AxumでリクエストボディのOptionalかつNullableなフィールドを区別して処理する方法

Last updated at Posted at 2023-12-24

REST APIで、リクエストボディ(JSON)の特定のフィールドについて、値あり・ null ・フィールドなしを区別して処理したい場合があります。

RustのWebアプリケーションフレームワークAxumでこれらを区別する際に、少し工夫が必要だったのでまとめます。

検証環境

[dependencies]
axum = "0.7.2"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
tokio = { version = "1.35.1", features = ["full"] }

コード

pub struct OptionalNullableField<T>(pub Option<Option<T>>);

/// OptionalNullableField<T>のフィールドまたはそれを含むstructに`#[serde(default)]`をつけると、
/// リクエストにフィールドが存在しないときに`OptionalNullableField(None)`になる
impl<T> Default for OptionalNullableField<T> {
    fn default() -> Self {
        Self(None)
    }
}

pub struct OptionalNullableFieldVisitor<T>(PhantomData<T>);

/// リクエストにフィールドが存在するときで、
/// 値がnullのときは`OptionalNullableField(Some(None))`、
/// null以外の値がセットされているときは`OptionalNullableField(Some(Some(value)))`となる
impl<'de, T> Visitor<'de> for OptionalNullableFieldVisitor<T>
where
    T: Deserialize<'de>,
{
    type Value = OptionalNullableField<T>;

    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
        formatter.write_str("null or T")
    }

    fn visit_none<E>(self) -> Result<Self::Value, E>
    where
        E: Error,
    {
        Ok(OptionalNullableField(Some(None)))
    }

    fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        T::deserialize(deserializer).map(|v| OptionalNullableField(Some(Some(v))))
    }
}

impl<'de, T> Deserialize<'de> for OptionalNullableField<T>
where
    T: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_option(OptionalNullableFieldVisitor(PhantomData))
    }
}

このようなデシリアライザを実装し、以下のようにリクエスト構造体を定義すると、使用できます。

#[derive(Deserialize)]
struct UpdateUser {
    #[serde(default)]
    username: OptionalNullableField<String>,
}

async fn update_user(Json(payload): Json<UpdateUser>) -> impl IntoResponse {
    match payload.username.0 {
        Some(Some(v)) => v, // 値がセットされているとき
        Some(None) => "null".to_string(), // nullがセットされているとき
        None => "none".to_string(), // フィールドが存在しないとき
    }
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0