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(), // フィールドが存在しないとき
}
}