dpp/serialization/json/
safe_integer.rs

1//! Serde `with` modules for bare u64/i64 fields and their Option variants.
2//!
3//! These are automatically added by the `#[json_safe_fields]` attribute macro.
4//! You should not normally need to reference them directly.
5//!
6//! ## Behavior
7//!
8//! - **JSON** (`is_human_readable() == true`): values > `MAX_SAFE_INTEGER` (2^53 - 1)
9//!   are serialized as strings. Deserialization accepts both numbers and strings.
10//! - **platform_value / bincode** (`is_human_readable() == false`): native integer
11//!   representation, no transformation.
12//!
13//! ## Available modules
14//!
15//! - [`json_safe_u64`] — for `u64` fields
16//! - [`json_safe_i64`] — for `i64` fields
17//! - [`json_safe_option_u64`] — for `Option<u64>` fields
18//! - [`json_safe_option_i64`] — for `Option<i64>` fields
19
20pub(crate) const JS_MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
21
22/// Serde `with` module for `u64` fields.
23pub mod json_safe_u64 {
24    use serde::de::{self, Deserializer, Visitor};
25    use serde::ser::Serializer;
26
27    use super::JS_MAX_SAFE_INTEGER;
28
29    pub fn serialize<S: Serializer>(value: &u64, serializer: S) -> Result<S::Ok, S::Error> {
30        if serializer.is_human_readable() && *value > JS_MAX_SAFE_INTEGER {
31            serializer.serialize_str(&value.to_string())
32        } else {
33            serializer.serialize_u64(*value)
34        }
35    }
36
37    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
38        if deserializer.is_human_readable() {
39            deserializer.deserialize_any(U64OrStringVisitor)
40        } else {
41            serde::Deserialize::deserialize(deserializer)
42        }
43    }
44
45    struct U64OrStringVisitor;
46
47    impl<'de> Visitor<'de> for U64OrStringVisitor {
48        type Value = u64;
49
50        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
51            formatter.write_str("a u64 or a string containing a u64")
52        }
53
54        fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
55            Ok(v)
56        }
57
58        fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
59            u64::try_from(v)
60                .map_err(|_| de::Error::custom(format!("i64 value {v} out of u64 range")))
61        }
62
63        fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
64            v.parse::<u64>()
65                .map_err(|_| de::Error::custom(format!("invalid u64 string: {v}")))
66        }
67    }
68}
69
70/// Serde `with` module for `i64` fields.
71pub mod json_safe_i64 {
72    use serde::de::{self, Deserializer, Visitor};
73    use serde::ser::Serializer;
74
75    use super::JS_MAX_SAFE_INTEGER;
76
77    const JS_MIN_SAFE_INTEGER: i64 = -(JS_MAX_SAFE_INTEGER as i64);
78
79    pub fn serialize<S: Serializer>(value: &i64, serializer: S) -> Result<S::Ok, S::Error> {
80        if serializer.is_human_readable()
81            && (*value > JS_MAX_SAFE_INTEGER as i64 || *value < JS_MIN_SAFE_INTEGER)
82        {
83            serializer.serialize_str(&value.to_string())
84        } else {
85            serializer.serialize_i64(*value)
86        }
87    }
88
89    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
90        if deserializer.is_human_readable() {
91            deserializer.deserialize_any(I64OrStringVisitor)
92        } else {
93            serde::Deserialize::deserialize(deserializer)
94        }
95    }
96
97    struct I64OrStringVisitor;
98
99    impl<'de> Visitor<'de> for I64OrStringVisitor {
100        type Value = i64;
101
102        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
103            formatter.write_str("an i64 or a string containing an i64")
104        }
105
106        fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
107            Ok(v)
108        }
109
110        fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
111            i64::try_from(v)
112                .map_err(|_| de::Error::custom(format!("u64 value {v} out of i64 range")))
113        }
114
115        fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
116            v.parse::<i64>()
117                .map_err(|_| de::Error::custom(format!("invalid i64 string: {v}")))
118        }
119    }
120}
121
122/// Serde `with` module for `Option<u64>` fields.
123pub mod json_safe_option_u64 {
124    use serde::de::{self, Deserializer, Visitor};
125    use serde::ser::Serializer;
126
127    pub fn serialize<S: Serializer>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error> {
128        match value {
129            Some(v) => super::json_safe_u64::serialize(v, serializer),
130            None => serializer.serialize_none(),
131        }
132    }
133
134    pub fn deserialize<'de, D: Deserializer<'de>>(
135        deserializer: D,
136    ) -> Result<Option<u64>, D::Error> {
137        if deserializer.is_human_readable() {
138            deserializer.deserialize_option(OptionU64Visitor)
139        } else {
140            serde::Deserialize::deserialize(deserializer)
141        }
142    }
143
144    struct OptionU64Visitor;
145
146    impl<'de> Visitor<'de> for OptionU64Visitor {
147        type Value = Option<u64>;
148
149        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
150            formatter.write_str("null, a u64, or a string containing a u64")
151        }
152
153        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
154            Ok(None)
155        }
156
157        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
158            Ok(None)
159        }
160
161        fn visit_some<D: Deserializer<'de>>(
162            self,
163            deserializer: D,
164        ) -> Result<Self::Value, D::Error> {
165            super::json_safe_u64::deserialize(deserializer).map(Some)
166        }
167    }
168}
169
170/// Serde `with` module for `Option<i64>` fields.
171pub mod json_safe_option_i64 {
172    use serde::de::{self, Deserializer, Visitor};
173    use serde::ser::Serializer;
174
175    pub fn serialize<S: Serializer>(value: &Option<i64>, serializer: S) -> Result<S::Ok, S::Error> {
176        match value {
177            Some(v) => super::json_safe_i64::serialize(v, serializer),
178            None => serializer.serialize_none(),
179        }
180    }
181
182    pub fn deserialize<'de, D: Deserializer<'de>>(
183        deserializer: D,
184    ) -> Result<Option<i64>, D::Error> {
185        if deserializer.is_human_readable() {
186            deserializer.deserialize_option(OptionI64Visitor)
187        } else {
188            serde::Deserialize::deserialize(deserializer)
189        }
190    }
191
192    struct OptionI64Visitor;
193
194    impl<'de> Visitor<'de> for OptionI64Visitor {
195        type Value = Option<i64>;
196
197        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
198            formatter.write_str("null, an i64, or a string containing an i64")
199        }
200
201        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
202            Ok(None)
203        }
204
205        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
206            Ok(None)
207        }
208
209        fn visit_some<D: Deserializer<'de>>(
210            self,
211            deserializer: D,
212        ) -> Result<Self::Value, D::Error> {
213            super::json_safe_i64::deserialize(deserializer).map(Some)
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use serde::{Deserialize, Serialize};
222
223    #[derive(Debug, PartialEq, Serialize, Deserialize)]
224    struct TestU64 {
225        #[serde(with = "json_safe_u64")]
226        value: u64,
227    }
228
229    #[derive(Debug, PartialEq, Serialize, Deserialize)]
230    struct TestI64 {
231        #[serde(with = "json_safe_i64")]
232        value: i64,
233    }
234
235    #[derive(Debug, PartialEq, Serialize, Deserialize)]
236    struct TestOptionU64 {
237        #[serde(default, with = "json_safe_option_u64")]
238        value: Option<u64>,
239    }
240
241    #[derive(Debug, PartialEq, Serialize, Deserialize)]
242    struct TestOptionI64 {
243        #[serde(default, with = "json_safe_option_i64")]
244        value: Option<i64>,
245    }
246
247    #[test]
248    fn u64_small_value_stays_number() {
249        let t = TestU64 { value: 42 };
250        let json = serde_json::to_value(&t).unwrap();
251        assert!(json["value"].is_number());
252        assert_eq!(json["value"].as_u64().unwrap(), 42);
253
254        let restored: TestU64 = serde_json::from_value(json).unwrap();
255        assert_eq!(t, restored);
256    }
257
258    #[test]
259    fn u64_large_value_becomes_string() {
260        let t = TestU64 { value: u64::MAX };
261        let json = serde_json::to_value(&t).unwrap();
262        assert!(json["value"].is_string());
263        assert_eq!(json["value"].as_str().unwrap(), "18446744073709551615");
264
265        let restored: TestU64 = serde_json::from_value(json).unwrap();
266        assert_eq!(t, restored);
267    }
268
269    #[test]
270    fn u64_at_max_safe_integer_stays_number() {
271        let t = TestU64 {
272            value: JS_MAX_SAFE_INTEGER,
273        };
274        let json = serde_json::to_value(&t).unwrap();
275        assert!(json["value"].is_number());
276    }
277
278    #[test]
279    fn u64_above_max_safe_integer_becomes_string() {
280        let t = TestU64 {
281            value: JS_MAX_SAFE_INTEGER + 1,
282        };
283        let json = serde_json::to_value(&t).unwrap();
284        assert!(json["value"].is_string());
285    }
286
287    #[test]
288    fn i64_small_value_stays_number() {
289        let t = TestI64 { value: -42 };
290        let json = serde_json::to_value(&t).unwrap();
291        assert!(json["value"].is_number());
292
293        let restored: TestI64 = serde_json::from_value(json).unwrap();
294        assert_eq!(t, restored);
295    }
296
297    #[test]
298    fn i64_large_value_becomes_string() {
299        let t = TestI64 { value: i64::MAX };
300        let json = serde_json::to_value(&t).unwrap();
301        assert!(json["value"].is_string());
302
303        let restored: TestI64 = serde_json::from_value(json).unwrap();
304        assert_eq!(t, restored);
305    }
306
307    #[test]
308    fn i64_large_negative_becomes_string() {
309        let t = TestI64 { value: i64::MIN };
310        let json = serde_json::to_value(&t).unwrap();
311        assert!(json["value"].is_string());
312
313        let restored: TestI64 = serde_json::from_value(json).unwrap();
314        assert_eq!(t, restored);
315    }
316
317    #[test]
318    fn option_u64_none_round_trip() {
319        let t = TestOptionU64 { value: None };
320        let json = serde_json::to_value(&t).unwrap();
321        assert!(json["value"].is_null());
322
323        let restored: TestOptionU64 = serde_json::from_value(json).unwrap();
324        assert_eq!(t, restored);
325    }
326
327    #[test]
328    fn option_u64_large_round_trip() {
329        let t = TestOptionU64 {
330            value: Some(u64::MAX),
331        };
332        let json = serde_json::to_value(&t).unwrap();
333        assert!(json["value"].is_string());
334
335        let restored: TestOptionU64 = serde_json::from_value(json).unwrap();
336        assert_eq!(t, restored);
337    }
338
339    #[test]
340    fn platform_value_u64_stays_native() {
341        let t = TestU64 { value: u64::MAX };
342        let pv = platform_value::to_value(&t).unwrap();
343
344        // platform_value is non-human-readable, so u64 stays as u64
345        let restored: TestU64 = platform_value::from_value(pv).unwrap();
346        assert_eq!(t, restored);
347    }
348
349    #[test]
350    fn option_i64_none_round_trip() {
351        let t = TestOptionI64 { value: None };
352        let json = serde_json::to_value(&t).unwrap();
353        assert!(json["value"].is_null());
354
355        let restored: TestOptionI64 = serde_json::from_value(json).unwrap();
356        assert_eq!(t, restored);
357    }
358
359    #[test]
360    fn option_i64_large_round_trip() {
361        let t = TestOptionI64 {
362            value: Some(i64::MAX),
363        };
364        let json = serde_json::to_value(&t).unwrap();
365        assert!(json["value"].is_string());
366
367        let restored: TestOptionI64 = serde_json::from_value(json).unwrap();
368        assert_eq!(t, restored);
369    }
370
371    #[test]
372    fn option_i64_large_negative_round_trip() {
373        let t = TestOptionI64 {
374            value: Some(i64::MIN),
375        };
376        let json = serde_json::to_value(&t).unwrap();
377        assert!(json["value"].is_string());
378
379        let restored: TestOptionI64 = serde_json::from_value(json).unwrap();
380        assert_eq!(t, restored);
381    }
382
383    #[test]
384    fn option_i64_missing_field_deserializes_as_none() {
385        let json = serde_json::json!({});
386        let restored: TestOptionI64 = serde_json::from_value(json).unwrap();
387        assert_eq!(restored.value, None);
388    }
389
390    #[test]
391    fn option_u64_missing_field_deserializes_as_none() {
392        let json = serde_json::json!({});
393        let restored: TestOptionU64 = serde_json::from_value(json).unwrap();
394        assert_eq!(restored.value, None);
395    }
396
397    #[test]
398    fn u64_deserialize_from_i64_value() {
399        // Tests visit_i64 path: JSON number that fits in i64 parsed as u64
400        let json = serde_json::json!({"value": 42});
401        let restored: TestU64 = serde_json::from_value(json).unwrap();
402        assert_eq!(restored.value, 42);
403    }
404
405    #[test]
406    fn u64_deserialize_negative_i64_fails() {
407        // Tests visit_i64 error path: negative i64 can't become u64
408        let json = serde_json::json!({"value": -1});
409        let result = serde_json::from_value::<TestU64>(json);
410        assert!(result.is_err());
411        assert!(result.unwrap_err().to_string().contains("out of u64 range"));
412    }
413
414    #[test]
415    fn u64_deserialize_invalid_string_fails() {
416        let json = serde_json::json!({"value": "not_a_number"});
417        let result = serde_json::from_value::<TestU64>(json);
418        assert!(result.is_err());
419        assert!(result
420            .unwrap_err()
421            .to_string()
422            .contains("invalid u64 string"));
423    }
424
425    #[test]
426    fn i64_deserialize_u64_overflow_fails() {
427        // Tests visit_u64 error path: u64::MAX can't fit in i64
428        let json = serde_json::json!({"value": u64::MAX.to_string()});
429        // This goes through visit_str which parses as i64 — will fail
430        let result = serde_json::from_value::<TestI64>(json);
431        assert!(result.is_err());
432    }
433
434    #[test]
435    fn i64_deserialize_invalid_string_fails() {
436        let json = serde_json::json!({"value": "not_a_number"});
437        let result = serde_json::from_value::<TestI64>(json);
438        assert!(result.is_err());
439        assert!(result
440            .unwrap_err()
441            .to_string()
442            .contains("invalid i64 string"));
443    }
444
445    #[test]
446    fn platform_value_i64_stays_native() {
447        let t = TestI64 { value: i64::MAX };
448        let pv = platform_value::to_value(&t).unwrap();
449        let restored: TestI64 = platform_value::from_value(pv).unwrap();
450        assert_eq!(t, restored);
451    }
452
453    #[test]
454    fn platform_value_option_u64_round_trip() {
455        let t = TestOptionU64 {
456            value: Some(u64::MAX),
457        };
458        let pv = platform_value::to_value(&t).unwrap();
459        let restored: TestOptionU64 = platform_value::from_value(pv).unwrap();
460        assert_eq!(t, restored);
461    }
462
463    #[test]
464    fn platform_value_option_i64_round_trip() {
465        let t = TestOptionI64 {
466            value: Some(i64::MIN),
467        };
468        let pv = platform_value::to_value(&t).unwrap();
469        let restored: TestOptionI64 = platform_value::from_value(pv).unwrap();
470        assert_eq!(t, restored);
471    }
472
473    #[test]
474    fn option_u64_small_value_stays_number() {
475        let t = TestOptionU64 { value: Some(42) };
476        let json = serde_json::to_value(&t).unwrap();
477        assert!(json["value"].is_number());
478
479        let restored: TestOptionU64 = serde_json::from_value(json).unwrap();
480        assert_eq!(t, restored);
481    }
482
483    #[test]
484    fn option_i64_small_value_stays_number() {
485        let t = TestOptionI64 { value: Some(-42) };
486        let json = serde_json::to_value(&t).unwrap();
487        assert!(json["value"].is_number());
488
489        let restored: TestOptionI64 = serde_json::from_value(json).unwrap();
490        assert_eq!(t, restored);
491    }
492
493    #[test]
494    fn tagged_enum_with_u64_round_trip() {
495        #[derive(Debug, PartialEq, Serialize, Deserialize)]
496        #[serde(tag = "$formatVersion")]
497        enum Versioned {
498            #[serde(rename = "0")]
499            V0(TestU64),
500        }
501
502        let v = Versioned::V0(TestU64 { value: u64::MAX });
503        let json = serde_json::to_value(&v).unwrap();
504        assert_eq!(json["$formatVersion"], "0");
505        assert!(json["value"].is_string());
506
507        let restored: Versioned = serde_json::from_value(json).unwrap();
508        assert_eq!(v, restored);
509    }
510}