boa_engine/value/conversions/
serde_json.rs1use 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 pub fn from_json(json: &Value, context: &mut Context) -> JsResult<Self> {
41 const MAX_INT: i64 = i32::MAX as i64;
43
44 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 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 None => arr.push(Value::Null),
164 }
165 }
166 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 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}