|
|
|
@ -680,6 +680,100 @@ where |
|
|
|
})) |
|
|
|
} |
|
|
|
|
|
|
|
/// Deserialize a field that can be absent, JSON `null`, or a string.
|
|
|
|
///
|
|
|
|
/// - Field absent → `None` (caller should leave the existing value unchanged)
|
|
|
|
/// - JSON `null` → `Some(None)` (caller should clear the value)
|
|
|
|
/// - JSON string → `Some(Some(value))`
|
|
|
|
///
|
|
|
|
/// Needed because plain `Option<String>` collapses both absent and `null` into `None`.
|
|
|
|
pub fn deser_double_opt_str<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error> |
|
|
|
where |
|
|
|
D: Deserializer<'de>, |
|
|
|
{ |
|
|
|
use serde::Deserialize; |
|
|
|
use serde::de::{self, Visitor}; |
|
|
|
use std::fmt; |
|
|
|
|
|
|
|
struct DoubleOptStrVisitor; |
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for DoubleOptStrVisitor { |
|
|
|
type Value = Option<Option<String>>; |
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
|
|
formatter.write_str("a string or null") |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> |
|
|
|
where |
|
|
|
E: de::Error, |
|
|
|
{ |
|
|
|
Ok(Some(Some(value.to_owned()))) |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_string<E>(self, value: String) -> Result<Self::Value, E> |
|
|
|
where |
|
|
|
E: de::Error, |
|
|
|
{ |
|
|
|
Ok(Some(Some(value))) |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_unit<E>(self) -> Result<Self::Value, E> |
|
|
|
where |
|
|
|
E: de::Error, |
|
|
|
{ |
|
|
|
Ok(Some(None)) |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_none<E>(self) -> Result<Self::Value, E> |
|
|
|
where |
|
|
|
E: de::Error, |
|
|
|
{ |
|
|
|
Ok(Some(None)) |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error> |
|
|
|
where |
|
|
|
D2: Deserializer<'de>, |
|
|
|
{ |
|
|
|
Ok(Some(Some(String::deserialize(deserializer)?))) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
deserializer.deserialize_option(DoubleOptStrVisitor) |
|
|
|
} |
|
|
|
|
|
|
|
#[cfg(test)] |
|
|
|
mod double_opt_str_tests { |
|
|
|
use super::deser_double_opt_str; |
|
|
|
use serde::Deserialize; |
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)] |
|
|
|
#[serde(rename_all = "camelCase")] |
|
|
|
struct Sample { |
|
|
|
#[serde(default, deserialize_with = "deser_double_opt_str")] |
|
|
|
archived_date: Option<Option<String>>, |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn absent_field_means_no_change() { |
|
|
|
let parsed: Sample = serde_json::from_str(r#"{"other":1}"#).unwrap(); |
|
|
|
assert_eq!(parsed.archived_date, None); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn null_field_means_clear() { |
|
|
|
let parsed: Sample = serde_json::from_str(r#"{"archivedDate":null}"#).unwrap(); |
|
|
|
assert_eq!(parsed.archived_date, Some(None)); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn string_field_means_set() { |
|
|
|
let parsed: Sample = serde_json::from_str(r#"{"archivedDate":"2026-08-11T12:00:00.000Z"}"#).unwrap(); |
|
|
|
assert_eq!(parsed.archived_date, Some(Some("2026-08-11T12:00:00.000Z".to_string()))); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Clone, Debug, Deserialize)] |
|
|
|
#[serde(untagged)] |
|
|
|
pub enum NumberOrString { |
|
|
|
|