Skip to main content

boa_engine/value/conversions/
serde_json.rs

1//! This module implements the conversions from and into [`serde_json::Value`].
2
3use super::JsValue;
4use crate::{
5    Context, JsResult, JsVariant,
6    builtins::Array,
7    error::JsNativeError,
8    js_string,
9    object::JsObject,
10    property::{PropertyDescriptor, PropertyKey},
11};
12use serde_json::{Map, Value};
13use std::collections::HashSet;
14
15impl JsValue {
16    /// Converts a [`serde_json::Value`] to a `JsValue`.
17    ///
18    /// # Example
19    ///
20    /// ```
21    /// use boa_engine::{Context, JsValue};
22    ///
23    /// let data = r#"
24    ///     {
25    ///         "name": "John Doe",
26    ///         "age": 43,
27    ///         "phones": [
28    ///             "+44 1234567",
29    ///             "+44 2345678"
30    ///         ]
31    ///      }"#;
32    ///
33    /// let json: serde_json::Value = serde_json::from_str(data).unwrap();
34    ///
35    /// let mut context = Context::default();
36    /// let value = JsValue::from_json(&json, &mut context).unwrap();
37    /// #
38    /// # assert_eq!(Some(json), value.to_json(&mut context).unwrap());
39    /// ```
40    pub fn from_json(json: &Value, context: &mut Context) -> JsResult<Self> {
41        /// Biggest possible integer, as i64.
42        const MAX_INT: i64 = i32::MAX as i64;
43
44        /// Smallest possible integer, as i64.
45        const MIN_INT: i64 = i32::MIN as i64;
46
47        match json {
48            Value::Null => Ok(Self::null()),
49            Value::Bool(b) => Ok(Self::new(*b)),
50            Value::Number(num) => num
51                .as_i64()
52                .filter(|n| (MIN_INT..=MAX_INT).contains(n))
53                .map(|i| Self::new(i as i32))
54                .or_else(|| num.as_f64().map(Self::new))
55                .ok_or_else(|| {
56                    JsNativeError::typ()
57                        .with_message(format!("could not convert JSON number {num} to JsValue"))
58                        .into()
59                }),
60            Value::String(string) => Ok(Self::from(js_string!(string.as_str()))),
61            Value::Array(vec) => {
62                let mut arr = Vec::with_capacity(vec.len());
63                for val in vec {
64                    arr.push(Self::from_json(val, context)?);
65                }
66                Ok(Array::create_array_from_list(arr, context).into())
67            }
68            Value::Object(obj) => {
69                let js_obj = JsObject::with_object_proto(context.intrinsics());
70                for (key, value) in obj {
71                    let property = PropertyDescriptor::builder()
72                        .value(Self::from_json(value, context)?)
73                        .writable(true)
74                        .enumerable(true)
75                        .configurable(true);
76                    js_obj
77                        .borrow_mut()
78                        .insert(js_string!(key.clone()), property);
79                }
80
81                Ok(js_obj.into())
82            }
83        }
84    }
85
86    /// Converts the `JsValue` to a [`serde_json::Value`].
87    ///
88    /// If the `JsValue` is `Undefined`, this method will return `None`.
89    /// Otherwise it will return the corresponding `serde_json::Value`.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// use boa_engine::{Context, JsValue};
95    ///
96    /// let data = r#"
97    ///     {
98    ///         "name": "John Doe",
99    ///         "age": 43,
100    ///         "phones": [
101    ///             "+44 1234567",
102    ///             "+44 2345678"
103    ///         ]
104    ///      }"#;
105    ///
106    /// let json: serde_json::Value = serde_json::from_str(data).unwrap();
107    ///
108    /// let mut context = Context::default();
109    /// let value = JsValue::from_json(&json, &mut context).unwrap();
110    ///
111    /// let back_to_json = value.to_json(&mut context).unwrap();
112    /// #
113    /// # assert_eq!(Some(json), back_to_json);
114    /// ```
115    pub fn to_json(&self, context: &mut Context) -> JsResult<Option<Value>> {
116        let mut seen_objects = HashSet::new();
117        self.to_json_inner(context, &mut seen_objects)
118    }
119
120    fn to_json_inner(
121        &self,
122        context: &mut Context,
123        seen_objects: &mut HashSet<JsObject>,
124    ) -> JsResult<Option<Value>> {
125        match self.variant() {
126            JsVariant::Null => Ok(Some(Value::Null)),
127            JsVariant::Undefined => Ok(None),
128            JsVariant::Boolean(b) => Ok(Some(Value::from(b))),
129            JsVariant::String(string) => Ok(Some(string.to_std_string_escaped().into())),
130            JsVariant::Float64(rat) => Ok(Some(Value::from(rat))),
131            JsVariant::Integer32(int) => Ok(Some(Value::from(int))),
132            JsVariant::BigInt(_bigint) => Err(JsNativeError::typ()
133                .with_message("cannot convert bigint to JSON")
134                .into()),
135            JsVariant::Object(obj) => {
136                if seen_objects.contains(&obj) {
137                    return Err(JsNativeError::typ()
138                        .with_message("cyclic object value")
139                        .into());
140                }
141                seen_objects.insert(obj.clone());
142                let mut value_by_prop_key = |property_key, context: &mut Context| {
143                    obj.borrow()
144                        .properties()
145                        .get(&property_key)
146                        .and_then(|x| {
147                            x.value()
148                                .map(|val| val.to_json_inner(context, seen_objects))
149                        })
150                        .unwrap_or(Ok(Some(Value::Null)))
151                };
152
153                if obj.is_array() {
154                    let len = obj.length_of_array_like(context)?;
155                    let mut arr = Vec::with_capacity(len as usize);
156
157                    for k in 0..len as u32 {
158                        let val = value_by_prop_key(k.into(), context)?;
159                        match val {
160                            Some(val) => arr.push(val),
161
162                            // Undefined in array. Substitute with null as Value doesn't support Undefined.
163                            None => arr.push(Value::Null),
164                        }
165                    }
166                    // Passing the object rather than its clone that was inserted to the set should be fine
167                    // as they hash to the same value and therefore HashSet can still remove the clone
168                    seen_objects.remove(&obj);
169                    Ok(Some(Value::Array(arr)))
170                } else {
171                    let mut map = Map::new();
172
173                    for index in obj.borrow().properties().index_property_keys() {
174                        let key = index.to_string();
175                        let value = value_by_prop_key(index.into(), context)?;
176                        if let Some(value) = value {
177                            map.insert(key, value);
178                        }
179                    }
180
181                    for property_key in obj.borrow().properties().shape.keys() {
182                        let key = match &property_key {
183                            PropertyKey::String(string) => string.to_std_string_escaped(),
184                            PropertyKey::Index(i) => i.get().to_string(),
185                            PropertyKey::Symbol(_sym) => {
186                                return Err(JsNativeError::typ()
187                                    .with_message("cannot convert Symbol to JSON")
188                                    .into());
189                            }
190                        };
191                        let value = value_by_prop_key(property_key, context)?;
192                        if let Some(value) = value {
193                            map.insert(key, value);
194                        }
195                    }
196                    seen_objects.remove(&obj);
197                    Ok(Some(Value::Object(map)))
198                }
199            }
200            JsVariant::Symbol(_sym) => Err(JsNativeError::typ()
201                .with_message("cannot convert Symbol to JSON")
202                .into()),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use boa_macros::js_str;
210    use indoc::indoc;
211    use serde_json::json;
212
213    use crate::{
214        Context, JsObject, JsValue, TestAction, js_string, object::JsArray, run_test_actions,
215    };
216
217    #[test]
218    fn json_conversions() {
219        const DATA: &str = indoc! {r#"
220            {
221                "name": "John Doe",
222                "age": 43,
223                "minor": false,
224                "adult": true,
225                "extra": {
226                    "address": null
227                },
228                "phones": [
229                    "+44 1234567",
230                    -45,
231                    {},
232                    true
233                ],
234                "7.3": "random text",
235                "100": 1000,
236                "24": 42
237            }
238        "#};
239
240        run_test_actions([TestAction::inspect_context(|ctx| {
241            let json: serde_json::Value = serde_json::from_str(DATA).unwrap();
242            assert!(json.is_object());
243
244            let value = JsValue::from_json(&json, ctx).unwrap();
245            let obj = value.as_object().unwrap();
246            assert_eq!(
247                obj.get(js_str!("name"), ctx).unwrap(),
248                js_str!("John Doe").into()
249            );
250            assert_eq!(obj.get(js_str!("age"), ctx).unwrap(), 43_i32.into());
251            assert_eq!(obj.get(js_str!("minor"), ctx).unwrap(), false.into());
252            assert_eq!(obj.get(js_str!("adult"), ctx).unwrap(), true.into());
253
254            assert_eq!(
255                obj.get(js_str!("7.3"), ctx).unwrap(),
256                js_string!("random text").into()
257            );
258            assert_eq!(obj.get(js_str!("100"), ctx).unwrap(), 1000.into());
259            assert_eq!(obj.get(js_str!("24"), ctx).unwrap(), 42.into());
260
261            {
262                let extra = obj.get(js_str!("extra"), ctx).unwrap();
263                let extra = extra.as_object().unwrap();
264                assert!(extra.get(js_str!("address"), ctx).unwrap().is_null());
265            }
266            {
267                let phones = obj.get(js_str!("phones"), ctx).unwrap();
268                let phones = phones.as_object().unwrap();
269
270                let arr = JsArray::from_object(phones.clone()).unwrap();
271                assert_eq!(arr.at(0, ctx).unwrap(), js_str!("+44 1234567").into());
272                assert_eq!(arr.at(1, ctx).unwrap(), JsValue::from(-45_i32));
273                assert!(arr.at(2, ctx).unwrap().is_object());
274                assert_eq!(arr.at(3, ctx).unwrap(), true.into());
275            }
276
277            assert_eq!(Some(json), value.to_json(ctx).unwrap());
278        })]);
279    }
280
281    #[test]
282    fn integer_ops_to_json() {
283        run_test_actions([
284            TestAction::assert_with_op("1000000 + 500", |v, ctx| {
285                v.to_json(ctx).unwrap() == Some(json!(1_000_500))
286            }),
287            TestAction::assert_with_op("1000000 - 500", |v, ctx| {
288                v.to_json(ctx).unwrap() == Some(json!(999_500))
289            }),
290            TestAction::assert_with_op("1000000 * 500", |v, ctx| {
291                v.to_json(ctx).unwrap() == Some(json!(500_000_000))
292            }),
293            TestAction::assert_with_op("1000000 / 500", |v, ctx| {
294                v.to_json(ctx).unwrap() == Some(json!(2_000))
295            }),
296            TestAction::assert_with_op("233894 % 500", |v, ctx| {
297                v.to_json(ctx).unwrap() == Some(json!(394))
298            }),
299            TestAction::assert_with_op("36 ** 5", |v, ctx| {
300                v.to_json(ctx).unwrap() == Some(json!(60_466_176))
301            }),
302        ]);
303    }
304
305    #[test]
306    fn to_json_cyclic() {
307        let mut context = Context::default();
308        let obj = JsObject::with_null_proto();
309        obj.create_data_property(js_string!("a"), obj.clone(), &mut context)
310            .expect("should create data property");
311
312        assert!(
313            JsValue::from(obj)
314                .to_json(&mut context)
315                .unwrap_err()
316                .to_string()
317                .starts_with("TypeError: cyclic object value"),
318        );
319    }
320
321    #[test]
322    fn to_json_undefined() {
323        let mut context = Context::default();
324        let undefined_value = JsValue::undefined();
325        assert!(undefined_value.to_json(&mut context).unwrap().is_none());
326    }
327
328    #[test]
329    fn to_json_undefined_in_structure() {
330        let mut context = Context::default();
331        let object_with_undefined = {
332            // Defining the following structure:
333            // {
334            //     "outer_a": 1,
335            //     "outer_b": undefined,
336            //     "outer_c": [2, undefined, 3, { "inner_a": undefined }]
337            // }
338
339            let inner = JsObject::with_null_proto();
340            inner
341                .create_data_property(js_string!("inner_a"), JsValue::undefined(), &mut context)
342                .expect("should add property");
343
344            let array = JsArray::new(&mut context).expect("creating array in test must not fail");
345            array.push(2, &mut context).expect("should push");
346            array
347                .push(JsValue::undefined(), &mut context)
348                .expect("should push");
349            array.push(3, &mut context).expect("should push");
350            array.push(inner, &mut context).expect("should push");
351
352            let outer = JsObject::with_null_proto();
353            outer
354                .create_data_property(js_string!("outer_a"), JsValue::new(1), &mut context)
355                .expect("should add property");
356            outer
357                .create_data_property(js_string!("outer_b"), JsValue::undefined(), &mut context)
358                .expect("should add property");
359            outer
360                .create_data_property(js_string!("outer_c"), array, &mut context)
361                .expect("should add property");
362
363            JsValue::from(outer)
364        };
365
366        assert_eq!(
367            Some(json!({
368                "outer_a": 1,
369                "outer_c": [2, null, 3, { }]
370            })),
371            object_with_undefined.to_json(&mut context).unwrap()
372        );
373    }
374}