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