Skip to main content

solana_nullable/
serde_with.rs

1//! [`serde_with`] integration for [`MaybeNull`].
2//!
3//! Provides blanket [`SerializeAs`] and [`DeserializeAs`] implementations so
4//! that any `serde_with` adapter can be composed with `MaybeNull<T>` using the
5//! `Option<Strategy>` pattern.
6//!
7//! ```rust
8//! # pub mod solana_address {
9//! #     #[derive(PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
10//! #     pub struct Address([u8; 32]);
11//! #     impl solana_nullable::Nullable for Address {
12//! #         const NONE: Self = Address([0u8; 32]);
13//! #     }
14//! #     impl core::str::FromStr for Address {
15//! #         type Err = String;
16//! #         fn from_str(s: &str) -> Result<Self, Self::Err> {
17//! #             Ok(Address([0u8; 32]))
18//! #         }
19//! #     }
20//! #     impl core::fmt::Display for Address {
21//! #         fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
22//! #             Ok(())
23//! #         }
24//! #     }
25//! # }
26//!
27//! use {
28//!     serde_derive::{Deserialize, Serialize},
29//!     serde_with::{serde_as, DisplayFromStr},
30//!     solana_address::Address,
31//!     solana_nullable::MaybeNull,
32//! };
33//!
34//! #[serde_as]
35//! #[derive(Serialize, Deserialize)]
36//! struct MyStruct {
37//!     #[serde_as(as = "Option<DisplayFromStr>")]
38//!     pub field: MaybeNull<Address>,
39//! }
40//!
41//! // Or without the proc macro:
42//! #[derive(Serialize, Deserialize)]
43//! struct MyStruct2 {
44//!     #[serde(with = "serde_with::As::<Option<DisplayFromStr>>")]
45//!     pub field: MaybeNull<Address>,
46//! }
47//! ```
48
49use {
50    crate::{MaybeNull, Nullable},
51    serde::{Deserialize, Deserializer, Serialize, Serializer},
52    serde_with::{de::DeserializeAsWrap, ser::SerializeAsWrap, DeserializeAs, SerializeAs},
53};
54
55impl<T, U> SerializeAs<MaybeNull<T>> for Option<U>
56where
57    T: Nullable,
58    U: SerializeAs<T>,
59{
60    fn serialize_as<S>(source: &MaybeNull<T>, serializer: S) -> Result<S::Ok, S::Error>
61    where
62        S: Serializer,
63    {
64        source
65            .as_ref()
66            .map(SerializeAsWrap::<T, U>::new)
67            .serialize(serializer)
68    }
69}
70
71impl<'de, T, U> DeserializeAs<'de, MaybeNull<T>> for Option<U>
72where
73    T: Nullable,
74    U: DeserializeAs<'de, T>,
75{
76    fn deserialize_as<D>(deserializer: D) -> Result<MaybeNull<T>, D::Error>
77    where
78        D: Deserializer<'de>,
79    {
80        Option::<DeserializeAsWrap<T, U>>::deserialize(deserializer)?
81            .map(DeserializeAsWrap::into_inner)
82            .try_into()
83            .map_err(serde::de::Error::custom)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use {
90        super::*,
91        crate::Nullable,
92        alloc::string::ToString,
93        serde_derive::{Deserialize, Serialize},
94        serde_with::{serde_as, DisplayFromStr},
95    };
96
97    #[derive(Clone, Copy, Debug, PartialEq)]
98    struct Id(u32);
99
100    impl Nullable for Id {
101        const NONE: Self = Id(0);
102    }
103
104    impl core::fmt::Display for Id {
105        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106            write!(f, "{}", self.0)
107        }
108    }
109
110    impl core::str::FromStr for Id {
111        type Err = core::num::ParseIntError;
112        fn from_str(s: &str) -> Result<Self, Self::Err> {
113            Ok(Id(s.parse()?))
114        }
115    }
116
117    #[serde_as]
118    #[derive(Debug, PartialEq, Serialize, Deserialize)]
119    struct TestStruct {
120        #[serde_as(as = "Option<DisplayFromStr>")]
121        pub value: MaybeNull<Id>,
122    }
123
124    #[test]
125    fn serialize_some_as_display_string() {
126        let s = TestStruct {
127            value: MaybeNull::from(Id(42)),
128        };
129        let json = serde_json::to_string(&s).unwrap();
130        assert_eq!(json, r#"{"value":"42"}"#);
131    }
132
133    #[test]
134    fn serialize_none_as_null() {
135        let s = TestStruct {
136            value: MaybeNull::default(),
137        };
138        let json = serde_json::to_string(&s).unwrap();
139        assert_eq!(json, r#"{"value":null}"#);
140    }
141
142    #[test]
143    fn deserialize_string_to_some() {
144        let json = r#"{"value":"42"}"#;
145        let s: TestStruct = serde_json::from_str(json).unwrap();
146        assert_eq!(s.value, MaybeNull::from(Id(42)));
147    }
148
149    #[test]
150    fn deserialize_null_to_none() {
151        let json = r#"{"value":null}"#;
152        let s: TestStruct = serde_json::from_str(json).unwrap();
153        assert_eq!(s.value, MaybeNull::default());
154    }
155
156    #[test]
157    fn deserialize_none_marker_in_some_is_rejected() {
158        // "0" parses to Id(0) which is the NONE marker
159        let json = r#"{"value":"0"}"#;
160        let err = serde_json::from_str::<TestStruct>(json).unwrap_err();
161        let msg = err.to_string();
162        assert!(msg.contains("None-equivalent"));
163    }
164
165    #[test]
166    fn deserialize_malformed_string_propagates_error() {
167        let json = r#"{"value":"not_a_number"}"#;
168        assert!(serde_json::from_str::<TestStruct>(json).is_err());
169    }
170
171    #[test]
172    fn deserialize_wrong_json_type_propagates_error() {
173        let json = r#"{"value":42}"#;
174        assert!(serde_json::from_str::<TestStruct>(json).is_err());
175    }
176
177    #[test]
178    fn roundtrip_some() {
179        let original = TestStruct {
180            value: MaybeNull::from(Id(99)),
181        };
182        let json = serde_json::to_string(&original).unwrap();
183        let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
184        assert_eq!(original, deserialized);
185    }
186
187    #[test]
188    fn deserialize_missing_field_is_error() {
189        // Without #[serde(default)], a missing key is an error, not null.
190        let json = r#"{}"#;
191        assert!(serde_json::from_str::<TestStruct>(json).is_err());
192    }
193
194    #[test]
195    fn roundtrip_none() {
196        let original = TestStruct {
197            value: MaybeNull::default(),
198        };
199        let json = serde_json::to_string(&original).unwrap();
200        let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
201        assert_eq!(original, deserialized);
202    }
203
204    // Verify it works with serde(with) syntax too
205    #[derive(Debug, PartialEq, Serialize, Deserialize)]
206    struct TestWithSyntax {
207        #[serde(with = "serde_with::As::<Option<DisplayFromStr>>")]
208        pub value: MaybeNull<Id>,
209    }
210
211    #[test]
212    fn serde_with_syntax_serialize_some() {
213        let s = TestWithSyntax {
214            value: MaybeNull::from(Id(7)),
215        };
216        let json = serde_json::to_string(&s).unwrap();
217        assert_eq!(json, r#"{"value":"7"}"#);
218    }
219
220    #[test]
221    fn serde_with_syntax_serialize_none() {
222        let s = TestWithSyntax {
223            value: MaybeNull::default(),
224        };
225        let json = serde_json::to_string(&s).unwrap();
226        assert_eq!(json, r#"{"value":null}"#);
227    }
228
229    #[test]
230    fn serde_with_syntax_deserialize_some() {
231        let json = r#"{"value":"7"}"#;
232        let s: TestWithSyntax = serde_json::from_str(json).unwrap();
233        assert_eq!(s.value, MaybeNull::from(Id(7)));
234    }
235
236    #[test]
237    fn serde_with_syntax_deserialize_null() {
238        let json = r#"{"value":null}"#;
239        let s: TestWithSyntax = serde_json::from_str(json).unwrap();
240        assert_eq!(s.value, MaybeNull::default());
241    }
242
243    // Verify a custom adapter works (not just DisplayFromStr)
244    #[derive(Clone, Copy, Debug, PartialEq)]
245    struct Score(u32);
246
247    impl Nullable for Score {
248        const NONE: Self = Score(0);
249    }
250
251    /// Custom adapter serializes Score as doubled value
252    struct DoubledScore;
253
254    impl SerializeAs<Score> for DoubledScore {
255        fn serialize_as<S>(source: &Score, serializer: S) -> Result<S::Ok, S::Error>
256        where
257            S: Serializer,
258        {
259            serializer.serialize_u32(source.0.saturating_mul(2))
260        }
261    }
262
263    impl<'de> DeserializeAs<'de, Score> for DoubledScore {
264        fn deserialize_as<D>(deserializer: D) -> Result<Score, D::Error>
265        where
266            D: Deserializer<'de>,
267        {
268            let v = u32::deserialize(deserializer)?;
269            Ok(Score(v / 2))
270        }
271    }
272
273    #[serde_as]
274    #[derive(Debug, PartialEq, Serialize, Deserialize)]
275    struct CustomAdapterStruct {
276        #[serde_as(as = "Option<DoubledScore>")]
277        pub value: MaybeNull<Score>,
278    }
279
280    #[test]
281    fn custom_adapter_serialize_some() {
282        let s = CustomAdapterStruct {
283            value: MaybeNull::from(Score(21)),
284        };
285        let json = serde_json::to_string(&s).unwrap();
286        assert_eq!(json, r#"{"value":42}"#);
287    }
288
289    #[test]
290    fn custom_adapter_serialize_none() {
291        let s = CustomAdapterStruct {
292            value: MaybeNull::default(),
293        };
294        let json = serde_json::to_string(&s).unwrap();
295        assert_eq!(json, r#"{"value":null}"#);
296    }
297
298    #[test]
299    fn custom_adapter_deserialize_some() {
300        let json = r#"{"value":42}"#;
301        let s: CustomAdapterStruct = serde_json::from_str(json).unwrap();
302        assert_eq!(
303            s,
304            CustomAdapterStruct {
305                value: MaybeNull::from(Score(21)),
306            }
307        );
308    }
309
310    #[test]
311    fn custom_adapter_deserialize_null() {
312        let json = r#"{"value":null}"#;
313        let s: CustomAdapterStruct = serde_json::from_str(json).unwrap();
314        assert_eq!(
315            s,
316            CustomAdapterStruct {
317                value: MaybeNull::default(),
318            }
319        );
320    }
321
322    #[test]
323    fn custom_adapter_rejects_none_marker_after_transform() {
324        // DoubledScore deserializes 1 as Score(1/2) = Score(0) which is NONE.
325        // The blanket impl must reject this even though the raw JSON value is valid.
326        let json = r#"{"value":1}"#;
327        let err = serde_json::from_str::<CustomAdapterStruct>(json).unwrap_err();
328        let msg = err.to_string();
329        assert!(msg.contains("None-equivalent"));
330    }
331}