Skip to main content

nodedb_types/value/
json.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Conversions between `Value` and `serde_json::Value`.
4
5use std::str::FromStr;
6
7use super::core::Value;
8
9impl From<Value> for serde_json::Value {
10    fn from(v: Value) -> Self {
11        match v {
12            Value::Null => serde_json::Value::Null,
13            Value::Bool(b) => serde_json::Value::Bool(b),
14            Value::Integer(i) => serde_json::json!(i),
15            // A non-finite float is its PostgreSQL text in a JSON string,
16            // never `null`.
17            Value::Float(f) => super::float_text::float_to_json(f),
18            Value::String(s) | Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => {
19                serde_json::Value::String(s)
20            }
21            Value::Bytes(b) => serde_json::Value::String(hex::encode(b)),
22            Value::Array(arr) | Value::Set(arr) => {
23                serde_json::Value::Array(arr.into_iter().map(serde_json::Value::from).collect())
24            }
25            Value::Object(map) => serde_json::Value::Object(
26                map.into_iter()
27                    .map(|(k, v)| (k, serde_json::Value::from(v)))
28                    .collect(),
29            ),
30            Value::DateTime(dt) | Value::NaiveDateTime(dt) => {
31                serde_json::Value::String(dt.to_string())
32            }
33            Value::Duration(d) => serde_json::Value::String(d.to_string()),
34            Value::Decimal(d) => {
35                // Represent as a JSON Number so clients see a numeric type,
36                // not a quoted string. `from_str` handles the decimal notation
37                // produced by rust_decimal's `to_string`. An integer past the
38                // 64-bit range parses to a rounded float, so it keeps its
39                // exact digits as a string.
40                let s = d.to_string();
41                match serde_json::Number::from_str(&s) {
42                    Ok(n) if !(d.scale() == 0 && n.is_f64()) => serde_json::Value::Number(n),
43                    _ => serde_json::Value::String(s),
44                }
45            }
46            Value::Geometry(g) => serde_json::to_value(g).unwrap_or(serde_json::Value::Null),
47            Value::Range { .. } | Value::Record { .. } => serde_json::Value::Null,
48            Value::Vector(v) => {
49                // A finite element keeps its shortest `f32` text. A
50                // non-finite one is its PostgreSQL text, never `null`.
51                serde_json::Value::Array(
52                    v.iter()
53                        .map(|f| {
54                            if f.is_finite() {
55                                serde_json::json!(*f)
56                            } else {
57                                super::float_text::float_to_json(f64::from(*f))
58                            }
59                        })
60                        .collect(),
61                )
62            }
63            Value::ArrayCell(cell) => {
64                let mut obj = serde_json::json!({
65                    "coords": cell.coords.into_iter().map(serde_json::Value::from).collect::<Vec<_>>(),
66                    "attrs": cell.attrs.into_iter().map(serde_json::Value::from).collect::<Vec<_>>(),
67                });
68                if let Some(ts) = cell.system_time {
69                    obj["_ts_system"] = serde_json::json!(ts);
70                }
71                obj
72            }
73        }
74    }
75}
76
77impl From<serde_json::Value> for Value {
78    fn from(v: serde_json::Value) -> Self {
79        match v {
80            serde_json::Value::Null => Value::Null,
81            serde_json::Value::Bool(b) => Value::Bool(b),
82            serde_json::Value::Number(n) => {
83                if let Some(i) = n.as_i64() {
84                    Value::Integer(i)
85                } else if let Some(u) = n.as_u64() {
86                    Value::from_u64(u)
87                } else if let Some(f) = n.as_f64() {
88                    Value::Float(f)
89                } else {
90                    Value::Null
91                }
92            }
93            serde_json::Value::String(s) => Value::String(s),
94            serde_json::Value::Array(arr) => {
95                Value::Array(arr.into_iter().map(Value::from).collect())
96            }
97            serde_json::Value::Object(map) => {
98                Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
99            }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::array_cell::ArrayCell;
108
109    #[test]
110    fn json_u64_above_i64_max_keeps_its_number() {
111        let json: serde_json::Value =
112            sonic_rs::from_str("18446744073709551615").expect("parse u64::MAX");
113        assert_eq!(
114            Value::from(json),
115            Value::Decimal(rust_decimal::Decimal::from(u64::MAX))
116        );
117        let small: serde_json::Value = sonic_rs::from_str("42").expect("parse 42");
118        assert_eq!(Value::from(small), Value::Integer(42));
119    }
120
121    #[test]
122    fn integer_decimal_past_64_bits_keeps_exact_digits() {
123        let big = rust_decimal::Decimal::from_i128_with_scale(2 * i128::from(u64::MAX), 0);
124        let json = serde_json::Value::from(Value::Decimal(big));
125        assert_eq!(
126            json,
127            serde_json::Value::String("36893488147419103230".into())
128        );
129        let max = serde_json::Value::from(Value::Decimal(rust_decimal::Decimal::from(u64::MAX)));
130        assert_eq!(max, serde_json::json!(u64::MAX));
131    }
132
133    #[test]
134    fn decimal_to_json_is_number_not_string() {
135        let d = rust_decimal::Decimal::new(12345, 2); // 123.45
136        let json = serde_json::Value::from(Value::Decimal(d));
137        assert!(json.is_number(), "expected JSON Number, got {json:?}");
138        assert_eq!(json.to_string(), "123.45");
139    }
140
141    // ── Documented-lossy JSON boundary conversions ────────────────────────
142    //
143    // These six variants lose type information when converted to JSON.
144    // The tests below pin the exact lossy behavior so future drift is caught.
145
146    #[test]
147    fn json_lossy_uuid_becomes_string() {
148        let v = Value::Uuid("550e8400-e29b-41d4-a716-446655440000".into());
149        let json = serde_json::Value::from(v);
150        assert!(json.is_string(), "Uuid must serialize as JSON string");
151        // Round-trip: comes back as String, not Uuid
152        let rt = Value::from(json);
153        assert!(
154            matches!(rt, Value::String(_)),
155            "Uuid round-trips through JSON as String, got {rt:?}"
156        );
157    }
158
159    #[test]
160    fn json_lossy_ulid_becomes_string() {
161        let v = Value::Ulid("01ARZ3NDEKTSV4RRFFQ69G5FAV".into());
162        let json = serde_json::Value::from(v);
163        assert!(json.is_string(), "Ulid must serialize as JSON string");
164        let rt = Value::from(json);
165        assert!(
166            matches!(rt, Value::String(_)),
167            "Ulid round-trips through JSON as String, got {rt:?}"
168        );
169    }
170
171    #[test]
172    fn json_lossy_regex_becomes_string() {
173        let v = Value::Regex(r"^\d+$".into());
174        let json = serde_json::Value::from(v);
175        assert!(json.is_string(), "Regex must serialize as JSON string");
176        let rt = Value::from(json);
177        assert!(
178            matches!(rt, Value::String(_)),
179            "Regex round-trips through JSON as String, got {rt:?}"
180        );
181    }
182
183    #[test]
184    fn json_lossy_range_becomes_null() {
185        let v = Value::Range {
186            start: Some(Box::new(Value::Integer(1))),
187            end: Some(Box::new(Value::Integer(10))),
188            inclusive: false,
189        };
190        let json = serde_json::Value::from(v);
191        assert!(
192            json.is_null(),
193            "Range must serialize as JSON null, got {json:?}"
194        );
195        let rt = Value::from(json);
196        assert!(
197            matches!(rt, Value::Null),
198            "Range round-trips through JSON as Null, got {rt:?}"
199        );
200    }
201
202    #[test]
203    fn json_lossy_record_becomes_null() {
204        let v = Value::Record {
205            table: "users".into(),
206            id: "abc123".into(),
207        };
208        let json = serde_json::Value::from(v);
209        assert!(
210            json.is_null(),
211            "Record must serialize as JSON null, got {json:?}"
212        );
213        let rt = Value::from(json);
214        assert!(
215            matches!(rt, Value::Null),
216            "Record round-trips through JSON as Null, got {rt:?}"
217        );
218    }
219
220    #[test]
221    fn json_lossy_array_cell_becomes_object_without_discriminator() {
222        let v = Value::ArrayCell(ArrayCell {
223            coords: vec![Value::Integer(1), Value::Integer(2)],
224            attrs: vec![Value::Float(3.5)],
225            system_time: None,
226        });
227        let json = serde_json::Value::from(v);
228        assert!(
229            json.is_object(),
230            "ArrayCell must serialize as JSON object, got {json:?}"
231        );
232        // The object has "coords" and "attrs" keys but no type discriminator.
233        let obj = json.as_object().unwrap();
234        assert!(
235            obj.contains_key("coords"),
236            "ArrayCell JSON must have 'coords' key"
237        );
238        assert!(
239            obj.contains_key("attrs"),
240            "ArrayCell JSON must have 'attrs' key"
241        );
242        // Round-trip comes back as Object, not ArrayCell.
243        let rt = Value::from(json);
244        assert!(
245            matches!(rt, Value::Object(_)),
246            "ArrayCell round-trips through JSON as Object, got {rt:?}"
247        );
248    }
249
250    /// A non-finite float renders as its PostgreSQL text in a JSON string,
251    /// at the top level, nested, and inside a vector.
252    #[test]
253    fn non_finite_floats_render_postgres_text_not_null() {
254        assert_eq!(
255            serde_json::Value::from(Value::Float(f64::NAN)),
256            serde_json::json!("NaN")
257        );
258        assert_eq!(
259            serde_json::Value::from(Value::Array(vec![
260                Value::Float(f64::INFINITY),
261                Value::Float(f64::NEG_INFINITY),
262                Value::Float(1.5),
263            ])),
264            serde_json::json!(["Infinity", "-Infinity", 1.5])
265        );
266        assert_eq!(
267            serde_json::Value::from(Value::Vector(vec![0.5, f32::NAN].into())),
268            serde_json::json!([0.5, "NaN"])
269        );
270    }
271}