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            Value::Float(f) => serde_json::json!(f),
16            Value::String(s) | Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => {
17                serde_json::Value::String(s)
18            }
19            Value::Bytes(b) => {
20                let hex: String = b.iter().map(|byte| format!("{byte:02x}")).collect();
21                serde_json::Value::String(hex)
22            }
23            Value::Array(arr) | Value::Set(arr) => {
24                serde_json::Value::Array(arr.into_iter().map(serde_json::Value::from).collect())
25            }
26            Value::Object(map) => serde_json::Value::Object(
27                map.into_iter()
28                    .map(|(k, v)| (k, serde_json::Value::from(v)))
29                    .collect(),
30            ),
31            Value::DateTime(dt) | Value::NaiveDateTime(dt) => {
32                serde_json::Value::String(dt.to_string())
33            }
34            Value::Duration(d) => serde_json::Value::String(d.to_string()),
35            Value::Decimal(d) => {
36                // Represent as a JSON Number so clients see a numeric type,
37                // not a quoted string. `from_str` handles the decimal notation
38                // produced by rust_decimal's `to_string`.
39                let s = d.to_string();
40                serde_json::Number::from_str(&s)
41                    .map(serde_json::Value::Number)
42                    .unwrap_or_else(|_| serde_json::Value::String(s))
43            }
44            Value::Geometry(g) => serde_json::to_value(g).unwrap_or(serde_json::Value::Null),
45            Value::Range { .. } | Value::Record { .. } => serde_json::Value::Null,
46            Value::Vector(v) => {
47                serde_json::Value::Array(v.iter().map(|f| serde_json::json!(*f)).collect())
48            }
49            Value::ArrayCell(cell) => serde_json::json!({
50                "coords": cell.coords.into_iter().map(serde_json::Value::from).collect::<Vec<_>>(),
51                "attrs": cell.attrs.into_iter().map(serde_json::Value::from).collect::<Vec<_>>(),
52            }),
53        }
54    }
55}
56
57impl From<serde_json::Value> for Value {
58    fn from(v: serde_json::Value) -> Self {
59        match v {
60            serde_json::Value::Null => Value::Null,
61            serde_json::Value::Bool(b) => Value::Bool(b),
62            serde_json::Value::Number(n) => {
63                if let Some(i) = n.as_i64() {
64                    Value::Integer(i)
65                } else if let Some(u) = n.as_u64() {
66                    Value::Integer(u as i64)
67                } else if let Some(f) = n.as_f64() {
68                    Value::Float(f)
69                } else {
70                    Value::Null
71                }
72            }
73            serde_json::Value::String(s) => Value::String(s),
74            serde_json::Value::Array(arr) => {
75                Value::Array(arr.into_iter().map(Value::from).collect())
76            }
77            serde_json::Value::Object(map) => {
78                Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
79            }
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::array_cell::ArrayCell;
88
89    #[test]
90    fn decimal_to_json_is_number_not_string() {
91        let d = rust_decimal::Decimal::new(12345, 2); // 123.45
92        let json = serde_json::Value::from(Value::Decimal(d));
93        assert!(json.is_number(), "expected JSON Number, got {json:?}");
94        assert_eq!(json.to_string(), "123.45");
95    }
96
97    // ── Documented-lossy JSON boundary conversions ────────────────────────
98    //
99    // These six variants lose type information when converted to JSON.
100    // The tests below pin the exact lossy behavior so future drift is caught.
101
102    #[test]
103    fn json_lossy_uuid_becomes_string() {
104        let v = Value::Uuid("550e8400-e29b-41d4-a716-446655440000".into());
105        let json = serde_json::Value::from(v);
106        assert!(json.is_string(), "Uuid must serialize as JSON string");
107        // Round-trip: comes back as String, not Uuid
108        let rt = Value::from(json);
109        assert!(
110            matches!(rt, Value::String(_)),
111            "Uuid round-trips through JSON as String, got {rt:?}"
112        );
113    }
114
115    #[test]
116    fn json_lossy_ulid_becomes_string() {
117        let v = Value::Ulid("01ARZ3NDEKTSV4RRFFQ69G5FAV".into());
118        let json = serde_json::Value::from(v);
119        assert!(json.is_string(), "Ulid must serialize as JSON string");
120        let rt = Value::from(json);
121        assert!(
122            matches!(rt, Value::String(_)),
123            "Ulid round-trips through JSON as String, got {rt:?}"
124        );
125    }
126
127    #[test]
128    fn json_lossy_regex_becomes_string() {
129        let v = Value::Regex(r"^\d+$".into());
130        let json = serde_json::Value::from(v);
131        assert!(json.is_string(), "Regex must serialize as JSON string");
132        let rt = Value::from(json);
133        assert!(
134            matches!(rt, Value::String(_)),
135            "Regex round-trips through JSON as String, got {rt:?}"
136        );
137    }
138
139    #[test]
140    fn json_lossy_range_becomes_null() {
141        let v = Value::Range {
142            start: Some(Box::new(Value::Integer(1))),
143            end: Some(Box::new(Value::Integer(10))),
144            inclusive: false,
145        };
146        let json = serde_json::Value::from(v);
147        assert!(
148            json.is_null(),
149            "Range must serialize as JSON null, got {json:?}"
150        );
151        let rt = Value::from(json);
152        assert!(
153            matches!(rt, Value::Null),
154            "Range round-trips through JSON as Null, got {rt:?}"
155        );
156    }
157
158    #[test]
159    fn json_lossy_record_becomes_null() {
160        let v = Value::Record {
161            table: "users".into(),
162            id: "abc123".into(),
163        };
164        let json = serde_json::Value::from(v);
165        assert!(
166            json.is_null(),
167            "Record must serialize as JSON null, got {json:?}"
168        );
169        let rt = Value::from(json);
170        assert!(
171            matches!(rt, Value::Null),
172            "Record round-trips through JSON as Null, got {rt:?}"
173        );
174    }
175
176    #[test]
177    fn json_lossy_array_cell_becomes_object_without_discriminator() {
178        let v = Value::ArrayCell(ArrayCell {
179            coords: vec![Value::Integer(1), Value::Integer(2)],
180            attrs: vec![Value::Float(3.5)],
181        });
182        let json = serde_json::Value::from(v);
183        assert!(
184            json.is_object(),
185            "ArrayCell must serialize as JSON object, got {json:?}"
186        );
187        // The object has "coords" and "attrs" keys but no type discriminator.
188        let obj = json.as_object().unwrap();
189        assert!(
190            obj.contains_key("coords"),
191            "ArrayCell JSON must have 'coords' key"
192        );
193        assert!(
194            obj.contains_key("attrs"),
195            "ArrayCell JSON must have 'attrs' key"
196        );
197        // Round-trip comes back as Object, not ArrayCell.
198        let rt = Value::from(json);
199        assert!(
200            matches!(rt, Value::Object(_)),
201            "ArrayCell round-trips through JSON as Object, got {rt:?}"
202        );
203    }
204}