Skip to main content

plexus_core/
serde_helpers.rs

1//! Serde helper utilities for consistent deserialization behavior
2//!
3//! These helpers are used by generated code to handle edge cases in JSON deserialization.
4
5use serde::{Deserialize, Deserializer};
6
7/// Deserializes an optional field that treats explicit `null` as `None`.
8///
9/// By default, serde with `#[serde(default)]` handles missing fields as `None`,
10/// but explicit `null` values cause deserialization errors for `Option<T>` when
11/// `T` doesn't accept null.
12///
13/// This deserializer treats both missing fields AND explicit `null` as `None`.
14///
15/// # Usage in generated code
16///
17/// ```ignore
18/// #[serde(default, deserialize_with = "hub_core::serde_helpers::deserialize_null_as_none")]
19/// field: Option<SomeType>
20/// ```
21///
22/// # Behavior
23///
24/// - Missing field -> `None` (via `#[serde(default)]`)
25/// - Explicit `null` -> `None`
26/// - Present value -> `Some(value)`
27pub fn deserialize_null_as_none<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
28where
29    T: Deserialize<'de>,
30    D: Deserializer<'de>,
31{
32    // Use Option<Option<T>> pattern: outer Option captures null vs present,
33    // inner Option is what we actually return
34    let opt = Option::<T>::deserialize(deserializer)?;
35    Ok(opt)
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use serde::Deserialize;
42
43    #[derive(Debug, Deserialize, PartialEq)]
44    struct TestStruct {
45        #[serde(default, deserialize_with = "deserialize_null_as_none")]
46        optional_string: Option<String>,
47
48        #[serde(default, deserialize_with = "deserialize_null_as_none")]
49        optional_number: Option<i32>,
50
51        required: String,
52    }
53
54    #[test]
55    fn test_missing_field() {
56        let json = r#"{"required": "test"}"#;
57        let parsed: TestStruct = serde_json::from_str(json).unwrap();
58        assert_eq!(parsed.optional_string, None);
59        assert_eq!(parsed.optional_number, None);
60        assert_eq!(parsed.required, "test");
61    }
62
63    #[test]
64    fn test_explicit_null() {
65        let json = r#"{"optional_string": null, "optional_number": null, "required": "test"}"#;
66        let parsed: TestStruct = serde_json::from_str(json).unwrap();
67        assert_eq!(parsed.optional_string, None);
68        assert_eq!(parsed.optional_number, None);
69        assert_eq!(parsed.required, "test");
70    }
71
72    #[test]
73    fn test_present_value() {
74        let json = r#"{"optional_string": "hello", "optional_number": 42, "required": "test"}"#;
75        let parsed: TestStruct = serde_json::from_str(json).unwrap();
76        assert_eq!(parsed.optional_string, Some("hello".to_string()));
77        assert_eq!(parsed.optional_number, Some(42));
78        assert_eq!(parsed.required, "test");
79    }
80
81    #[test]
82    fn test_mixed_null_and_present() {
83        let json = r#"{"optional_string": null, "optional_number": 42, "required": "test"}"#;
84        let parsed: TestStruct = serde_json::from_str(json).unwrap();
85        assert_eq!(parsed.optional_string, None);
86        assert_eq!(parsed.optional_number, Some(42));
87        assert_eq!(parsed.required, "test");
88    }
89
90    // Test with a complex inner type
91    #[derive(Debug, Deserialize, PartialEq)]
92    struct InnerType {
93        value: String,
94    }
95
96    #[derive(Debug, Deserialize, PartialEq)]
97    struct TestWithComplexInner {
98        #[serde(default, deserialize_with = "deserialize_null_as_none")]
99        complex: Option<InnerType>,
100    }
101
102    #[test]
103    fn test_complex_inner_missing() {
104        let json = r#"{}"#;
105        let parsed: TestWithComplexInner = serde_json::from_str(json).unwrap();
106        assert_eq!(parsed.complex, None);
107    }
108
109    #[test]
110    fn test_complex_inner_null() {
111        let json = r#"{"complex": null}"#;
112        let parsed: TestWithComplexInner = serde_json::from_str(json).unwrap();
113        assert_eq!(parsed.complex, None);
114    }
115
116    #[test]
117    fn test_complex_inner_present() {
118        let json = r#"{"complex": {"value": "hello"}}"#;
119        let parsed: TestWithComplexInner = serde_json::from_str(json).unwrap();
120        assert_eq!(
121            parsed.complex,
122            Some(InnerType {
123                value: "hello".to_string()
124            })
125        );
126    }
127}