Skip to main content

shape_runtime/stdlib/
json.rs

1//! Native `json` module for JSON parsing and serialization.
2//!
3//! Exports: json.parse(text), json.stringify(value, pretty?), json.is_valid(text)
4
5use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use crate::type_schema::{SchemaId, TypeSchemaRegistry, nb_to_slot};
7use shape_value::heap_value::HeapValue;
8use shape_value::{ValueSlot, ValueWord};
9use std::sync::Arc;
10
11/// Convert a `serde_json::Value` into an untyped `ValueWord` (legacy fallback).
12fn json_value_to_nanboxed(value: serde_json::Value) -> ValueWord {
13    match value {
14        serde_json::Value::Null => ValueWord::none(),
15        serde_json::Value::Bool(b) => ValueWord::from_bool(b),
16        serde_json::Value::Number(n) => ValueWord::from_f64(n.as_f64().unwrap_or(0.0)),
17        serde_json::Value::String(s) => ValueWord::from_string(Arc::new(s)),
18        serde_json::Value::Array(arr) => {
19            let items: Vec<ValueWord> = arr.into_iter().map(json_value_to_nanboxed).collect();
20            ValueWord::from_array(Arc::new(items))
21        }
22        serde_json::Value::Object(map) => {
23            let mut keys = Vec::with_capacity(map.len());
24            let mut values = Vec::with_capacity(map.len());
25            for (k, v) in map.into_iter() {
26                keys.push(ValueWord::from_string(Arc::new(k)));
27                values.push(json_value_to_nanboxed(v));
28            }
29            ValueWord::from_hashmap_pairs(keys, values)
30        }
31    }
32}
33
34// Json enum variant IDs (must match order in json_value.shape)
35const JSON_VARIANT_NULL: i64 = 0;
36const JSON_VARIANT_BOOL: i64 = 1;
37const JSON_VARIANT_NUMBER: i64 = 2;
38const JSON_VARIANT_STR: i64 = 3;
39const JSON_VARIANT_ARRAY: i64 = 4;
40const JSON_VARIANT_OBJECT: i64 = 5;
41
42/// Build a Json enum TypedObject with the given variant and payload.
43fn make_json_enum(schema_id: u64, variant_id: i64, payload: Option<ValueWord>) -> ValueWord {
44    // Json layout: slot 0 = __variant (I64), slot 1 = __payload_0 (Any)
45    let variant_slot = ValueSlot::from_int(variant_id);
46    let (payload_slot, heap_mask) = if let Some(ref p) = payload {
47        let (slot, is_heap) = nb_to_slot(p);
48        (slot, if is_heap { 1u64 << 1 } else { 0u64 })
49    } else {
50        (ValueSlot::none(), 0u64)
51    };
52    let slots = vec![variant_slot, payload_slot].into_boxed_slice();
53    ValueWord::from_heap_value(HeapValue::TypedObject {
54        schema_id,
55        slots,
56        heap_mask,
57    })
58}
59
60/// Convert a `serde_json::Value` into a typed `Json` enum TypedObject.
61fn json_value_to_enum(value: serde_json::Value, schema_id: u64) -> ValueWord {
62    match value {
63        serde_json::Value::Null => make_json_enum(schema_id, JSON_VARIANT_NULL, None),
64        serde_json::Value::Bool(b) => {
65            make_json_enum(schema_id, JSON_VARIANT_BOOL, Some(ValueWord::from_bool(b)))
66        }
67        serde_json::Value::Number(n) => make_json_enum(
68            schema_id,
69            JSON_VARIANT_NUMBER,
70            Some(ValueWord::from_f64(n.as_f64().unwrap_or(0.0))),
71        ),
72        serde_json::Value::String(s) => make_json_enum(
73            schema_id,
74            JSON_VARIANT_STR,
75            Some(ValueWord::from_string(Arc::new(s))),
76        ),
77        serde_json::Value::Array(arr) => {
78            let items: Vec<ValueWord> = arr
79                .into_iter()
80                .map(|v| json_value_to_enum(v, schema_id))
81                .collect();
82            make_json_enum(
83                schema_id,
84                JSON_VARIANT_ARRAY,
85                Some(ValueWord::from_array(Arc::new(items))),
86            )
87        }
88        serde_json::Value::Object(map) => {
89            let mut keys = Vec::with_capacity(map.len());
90            let mut values = Vec::with_capacity(map.len());
91            for (k, v) in map.into_iter() {
92                keys.push(ValueWord::from_string(Arc::new(k)));
93                values.push(json_value_to_enum(v, schema_id));
94            }
95            make_json_enum(
96                schema_id,
97                JSON_VARIANT_OBJECT,
98                Some(ValueWord::from_hashmap_pairs(keys, values)),
99            )
100        }
101    }
102}
103
104/// Convert a `serde_json::Value` into a typed struct `ValueWord` using a schema.
105///
106/// Matches JSON keys to schema fields using `wire_name()` (respects `@alias`).
107fn json_object_to_typed(
108    schema_id: SchemaId,
109    schema: &crate::type_schema::TypeSchema,
110    map: &serde_json::Map<String, serde_json::Value>,
111    registry: &TypeSchemaRegistry,
112) -> Result<ValueWord, String> {
113    use crate::type_schema::FieldType;
114
115    let num_fields = schema.fields.len();
116    let mut slots = vec![ValueSlot::none(); num_fields];
117    let mut heap_mask = 0u64;
118
119    for field in &schema.fields {
120        let wire = field.wire_name();
121        let json_val = map.get(wire);
122        let nb = if let Some(jv) = json_val {
123            json_value_to_typed_nb(jv, &field.field_type, registry)?
124        } else {
125            ValueWord::none()
126        };
127
128        // Convert ValueWord to ValueSlot based on field type
129        let (slot, is_heap) = match &field.field_type {
130            FieldType::I64 => (
131                ValueSlot::from_int(
132                    nb.as_i64()
133                        .or_else(|| nb.as_f64().map(|n| n as i64))
134                        .unwrap_or(0),
135                ),
136                false,
137            ),
138            FieldType::Bool => (ValueSlot::from_bool(nb.as_bool().unwrap_or(false)), false),
139            FieldType::F64 | FieldType::Decimal => (
140                ValueSlot::from_number(nb.as_number_coerce().unwrap_or(0.0)),
141                false,
142            ),
143            _ => nb_to_slot(&nb),
144        };
145
146        slots[field.index as usize] = slot;
147        if is_heap {
148            heap_mask |= 1u64 << field.index;
149        }
150    }
151
152    Ok(ValueWord::from_heap_value(HeapValue::TypedObject {
153        schema_id: schema_id as u64,
154        slots: slots.into_boxed_slice(),
155        heap_mask,
156    }))
157}
158
159/// Convert a single JSON value to a ValueWord according to the field type.
160fn json_value_to_typed_nb(
161    value: &serde_json::Value,
162    field_type: &crate::type_schema::FieldType,
163    registry: &TypeSchemaRegistry,
164) -> Result<ValueWord, String> {
165    use crate::type_schema::FieldType;
166    match (value, field_type) {
167        (serde_json::Value::Null, _) => Ok(ValueWord::none()),
168        (serde_json::Value::Bool(b), _) => Ok(ValueWord::from_bool(*b)),
169        (serde_json::Value::Number(n), FieldType::I64) => {
170            Ok(ValueWord::from_i64(n.as_i64().unwrap_or(0)))
171        }
172        (serde_json::Value::Number(n), _) => Ok(ValueWord::from_f64(n.as_f64().unwrap_or(0.0))),
173        (serde_json::Value::String(s), _) => Ok(ValueWord::from_string(Arc::new(s.clone()))),
174        (serde_json::Value::Array(arr), _) => {
175            let items: Vec<ValueWord> = arr
176                .iter()
177                .map(|v| json_value_to_typed_nb(v, &FieldType::Any, registry))
178                .collect::<Result<_, _>>()?;
179            Ok(ValueWord::from_array(Arc::new(items)))
180        }
181        (serde_json::Value::Object(obj), FieldType::Object(type_name)) => {
182            if let Some(nested_schema) = registry.get(type_name) {
183                json_object_to_typed(nested_schema.id, nested_schema, obj, registry)
184            } else {
185                // Schema not found — fall back to untyped hashmap
186                Ok(json_value_to_nanboxed(serde_json::Value::Object(
187                    obj.clone(),
188                )))
189            }
190        }
191        (serde_json::Value::Object(obj), _) => Ok(json_value_to_nanboxed(
192            serde_json::Value::Object(obj.clone()),
193        )),
194    }
195}
196
197/// Create the `json` module with JSON parsing and serialization functions.
198pub fn create_json_module() -> ModuleExports {
199    let mut module = ModuleExports::new("json");
200    module.description = "JSON parsing and serialization".to_string();
201
202    // json.parse(text: string) -> Result<Json>
203    // Returns typed Json enum when the schema is registered, otherwise untyped.
204    module.add_function_with_schema(
205        "parse",
206        |args: &[ValueWord], ctx: &ModuleContext| {
207            let text = args
208                .first()
209                .and_then(|a| a.as_str())
210                .ok_or_else(|| "json.parse() requires a string argument".to_string())?;
211
212            let parsed: serde_json::Value =
213                serde_json::from_str(text).map_err(|e| format!("json.parse() failed: {}", e))?;
214
215            let result = if let Some(json_schema) = ctx.schemas.get("Json") {
216                json_value_to_enum(parsed, json_schema.id as u64)
217            } else {
218                json_value_to_nanboxed(parsed)
219            };
220
221            Ok(ValueWord::from_ok(result))
222        },
223        ModuleFunction {
224            description: "Parse a JSON string into Shape values".to_string(),
225            params: vec![ModuleParam {
226                name: "text".to_string(),
227                type_name: "string".to_string(),
228                required: true,
229                description: "JSON string to parse".to_string(),
230                ..Default::default()
231            }],
232            return_type: Some("Result<Json>".to_string()),
233        },
234    );
235
236    // json.__parse_typed(text: string, schema_id: number) -> Result<T>
237    // Internal: deserializes JSON directly into a typed struct using the schema.
238    module.add_function_with_schema(
239        "__parse_typed",
240        |args: &[ValueWord], ctx: &ModuleContext| {
241            let text = args
242                .first()
243                .and_then(|a| a.as_str())
244                .ok_or_else(|| "json.__parse_typed() requires a string argument".to_string())?;
245            let schema_id = args
246                .get(1)
247                .and_then(|a| {
248                    a.as_f64()
249                        .map(|n| n as u32)
250                        .or_else(|| a.as_i64().map(|n| n as u32))
251                })
252                .ok_or_else(|| "json.__parse_typed() requires a schema_id argument".to_string())?;
253
254            let parsed: serde_json::Value = serde_json::from_str(text)
255                .map_err(|e| format!("json.__parse_typed() failed: {}", e))?;
256
257            let map = match parsed {
258                serde_json::Value::Object(m) => m,
259                _ => {
260                    return Err("json.__parse_typed() requires a JSON object".to_string());
261                }
262            };
263
264            let schema = ctx
265                .schemas
266                .get_by_id(schema_id)
267                .ok_or_else(|| format!("json.__parse_typed(): unknown schema id {}", schema_id))?;
268
269            let result = json_object_to_typed(schema_id, schema, &map, ctx.schemas)?;
270            Ok(ValueWord::from_ok(result))
271        },
272        ModuleFunction {
273            description: "Parse a JSON string into a typed struct".to_string(),
274            params: vec![
275                ModuleParam {
276                    name: "text".to_string(),
277                    type_name: "string".to_string(),
278                    required: true,
279                    description: "JSON string to parse".to_string(),
280                    ..Default::default()
281                },
282                ModuleParam {
283                    name: "schema_id".to_string(),
284                    type_name: "number".to_string(),
285                    required: true,
286                    description: "Schema ID of the target type".to_string(),
287                    ..Default::default()
288                },
289            ],
290            return_type: Some("Result<any>".to_string()),
291        },
292    );
293
294    // json.stringify(value: any, pretty?: bool) -> Result<string>
295    module.add_function_with_schema(
296        "stringify",
297        |args: &[ValueWord], _ctx: &ModuleContext| {
298            let value = args
299                .first()
300                .ok_or_else(|| "json.stringify() requires a value argument".to_string())?;
301
302            let pretty = args.get(1).and_then(|a| a.as_bool()).unwrap_or(false);
303
304            let json_value = value.to_json_value();
305
306            let output = if pretty {
307                serde_json::to_string_pretty(&json_value)
308            } else {
309                serde_json::to_string(&json_value)
310            }
311            .map_err(|e| format!("json.stringify() failed: {}", e))?;
312
313            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(output))))
314        },
315        ModuleFunction {
316            description: "Serialize a Shape value to a JSON string".to_string(),
317            params: vec![
318                ModuleParam {
319                    name: "value".to_string(),
320                    type_name: "any".to_string(),
321                    required: true,
322                    description: "Value to serialize".to_string(),
323                    ..Default::default()
324                },
325                ModuleParam {
326                    name: "pretty".to_string(),
327                    type_name: "bool".to_string(),
328                    required: false,
329                    description: "Pretty-print with indentation (default: false)".to_string(),
330                    default_snippet: Some("false".to_string()),
331                    ..Default::default()
332                },
333            ],
334            return_type: Some("Result<string>".to_string()),
335        },
336    );
337
338    // json.is_valid(text: string) -> bool
339    module.add_function_with_schema(
340        "is_valid",
341        |args: &[ValueWord], _ctx: &ModuleContext| {
342            let text = args
343                .first()
344                .and_then(|a| a.as_str())
345                .ok_or_else(|| "json.is_valid() requires a string argument".to_string())?;
346
347            let valid = serde_json::from_str::<serde_json::Value>(text).is_ok();
348            Ok(ValueWord::from_bool(valid))
349        },
350        ModuleFunction {
351            description: "Check if a string is valid JSON".to_string(),
352            params: vec![ModuleParam {
353                name: "text".to_string(),
354                type_name: "string".to_string(),
355                required: true,
356                description: "String to validate as JSON".to_string(),
357                ..Default::default()
358            }],
359            return_type: Some("bool".to_string()),
360        },
361    );
362
363    module
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
371        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
372        crate::module_exports::ModuleContext {
373            schemas: registry,
374            invoke_callable: None,
375            raw_invoker: None,
376            function_hashes: None,
377            vm_state: None,
378            granted_permissions: None,
379            scope_constraints: None,
380            set_pending_resume: None,
381            set_pending_frame_resume: None,
382        }
383    }
384
385    #[test]
386    fn test_json_module_creation() {
387        let module = create_json_module();
388        assert_eq!(module.name, "json");
389        assert!(module.has_export("parse"));
390        assert!(module.has_export("stringify"));
391        assert!(module.has_export("is_valid"));
392    }
393
394    #[test]
395    fn test_json_parse_string() {
396        let module = create_json_module();
397        let parse_fn = module.get_export("parse").unwrap();
398        let ctx = test_ctx();
399        let input = ValueWord::from_string(Arc::new(r#""hello""#.to_string()));
400        let result = parse_fn(&[input], &ctx).unwrap();
401        // Result is Ok(value)
402        let inner = result.as_ok_inner().expect("should be Ok");
403        assert_eq!(inner.as_str(), Some("hello"));
404    }
405
406    #[test]
407    fn test_json_parse_number() {
408        let module = create_json_module();
409        let parse_fn = module.get_export("parse").unwrap();
410        let ctx = test_ctx();
411        let input = ValueWord::from_string(Arc::new("42.5".to_string()));
412        let result = parse_fn(&[input], &ctx).unwrap();
413        let inner = result.as_ok_inner().expect("should be Ok");
414        assert_eq!(inner.as_f64(), Some(42.5));
415    }
416
417    #[test]
418    fn test_json_parse_bool() {
419        let module = create_json_module();
420        let parse_fn = module.get_export("parse").unwrap();
421        let ctx = test_ctx();
422        let input = ValueWord::from_string(Arc::new("true".to_string()));
423        let result = parse_fn(&[input], &ctx).unwrap();
424        let inner = result.as_ok_inner().expect("should be Ok");
425        assert_eq!(inner.as_bool(), Some(true));
426    }
427
428    #[test]
429    fn test_json_parse_null() {
430        let module = create_json_module();
431        let parse_fn = module.get_export("parse").unwrap();
432        let ctx = test_ctx();
433        let input = ValueWord::from_string(Arc::new("null".to_string()));
434        let result = parse_fn(&[input], &ctx).unwrap();
435        let inner = result.as_ok_inner().expect("should be Ok");
436        assert!(inner.is_none());
437    }
438
439    #[test]
440    fn test_json_parse_array() {
441        let module = create_json_module();
442        let parse_fn = module.get_export("parse").unwrap();
443        let ctx = test_ctx();
444        let input = ValueWord::from_string(Arc::new("[1, 2, 3]".to_string()));
445        let result = parse_fn(&[input], &ctx).unwrap();
446        let inner = result.as_ok_inner().expect("should be Ok");
447        let arr = inner.as_any_array().expect("should be array").to_generic();
448        assert_eq!(arr.len(), 3);
449        assert_eq!(arr[0].as_f64(), Some(1.0));
450        assert_eq!(arr[1].as_f64(), Some(2.0));
451        assert_eq!(arr[2].as_f64(), Some(3.0));
452    }
453
454    #[test]
455    fn test_json_parse_object() {
456        let module = create_json_module();
457        let parse_fn = module.get_export("parse").unwrap();
458        let ctx = test_ctx();
459        let input = ValueWord::from_string(Arc::new(r#"{"a": 1, "b": "two"}"#.to_string()));
460        let result = parse_fn(&[input], &ctx).unwrap();
461        let inner = result.as_ok_inner().expect("should be Ok");
462        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
463        assert_eq!(keys.len(), 2);
464    }
465
466    #[test]
467    fn test_json_parse_invalid() {
468        let module = create_json_module();
469        let parse_fn = module.get_export("parse").unwrap();
470        let ctx = test_ctx();
471        let input = ValueWord::from_string(Arc::new("{invalid}".to_string()));
472        let result = parse_fn(&[input], &ctx);
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn test_json_parse_requires_string() {
478        let module = create_json_module();
479        let parse_fn = module.get_export("parse").unwrap();
480        let ctx = test_ctx();
481        let result = parse_fn(&[ValueWord::from_f64(42.0)], &ctx);
482        assert!(result.is_err());
483    }
484
485    #[test]
486    fn test_json_stringify_number() {
487        let module = create_json_module();
488        let stringify_fn = module.get_export("stringify").unwrap();
489        let ctx = test_ctx();
490        let result = stringify_fn(&[ValueWord::from_f64(42.0)], &ctx).unwrap();
491        let inner = result.as_ok_inner().expect("should be Ok");
492        assert_eq!(inner.as_str(), Some("42.0"));
493    }
494
495    #[test]
496    fn test_json_stringify_string() {
497        let module = create_json_module();
498        let stringify_fn = module.get_export("stringify").unwrap();
499        let ctx = test_ctx();
500        let result = stringify_fn(
501            &[ValueWord::from_string(Arc::new("hello".to_string()))],
502            &ctx,
503        )
504        .unwrap();
505        let inner = result.as_ok_inner().expect("should be Ok");
506        assert_eq!(inner.as_str(), Some("\"hello\""));
507    }
508
509    #[test]
510    fn test_json_stringify_bool() {
511        let module = create_json_module();
512        let stringify_fn = module.get_export("stringify").unwrap();
513        let ctx = test_ctx();
514        let result = stringify_fn(&[ValueWord::from_bool(true)], &ctx).unwrap();
515        let inner = result.as_ok_inner().expect("should be Ok");
516        assert_eq!(inner.as_str(), Some("true"));
517    }
518
519    #[test]
520    fn test_json_stringify_none() {
521        let module = create_json_module();
522        let stringify_fn = module.get_export("stringify").unwrap();
523        let ctx = test_ctx();
524        let result = stringify_fn(&[ValueWord::none()], &ctx).unwrap();
525        let inner = result.as_ok_inner().expect("should be Ok");
526        assert_eq!(inner.as_str(), Some("null"));
527    }
528
529    #[test]
530    fn test_json_stringify_array() {
531        let module = create_json_module();
532        let stringify_fn = module.get_export("stringify").unwrap();
533        let ctx = test_ctx();
534        let arr = ValueWord::from_array(Arc::new(vec![
535            ValueWord::from_f64(1.0),
536            ValueWord::from_f64(2.0),
537        ]));
538        let result = stringify_fn(&[arr], &ctx).unwrap();
539        let inner = result.as_ok_inner().expect("should be Ok");
540        assert_eq!(inner.as_str(), Some("[1.0,2.0]"));
541    }
542
543    #[test]
544    fn test_json_stringify_pretty() {
545        let module = create_json_module();
546        let stringify_fn = module.get_export("stringify").unwrap();
547        let ctx = test_ctx();
548        let result = stringify_fn(
549            &[ValueWord::from_f64(42.0), ValueWord::from_bool(true)],
550            &ctx,
551        )
552        .unwrap();
553        let inner = result.as_ok_inner().expect("should be Ok");
554        // Pretty mode with a single number is the same as compact
555        assert_eq!(inner.as_str(), Some("42.0"));
556    }
557
558    #[test]
559    fn test_json_is_valid_true() {
560        let module = create_json_module();
561        let is_valid_fn = module.get_export("is_valid").unwrap();
562        let ctx = test_ctx();
563        let result = is_valid_fn(
564            &[ValueWord::from_string(Arc::new(
565                r#"{"key": "value"}"#.to_string(),
566            ))],
567            &ctx,
568        )
569        .unwrap();
570        assert_eq!(result.as_bool(), Some(true));
571    }
572
573    #[test]
574    fn test_json_is_valid_false() {
575        let module = create_json_module();
576        let is_valid_fn = module.get_export("is_valid").unwrap();
577        let ctx = test_ctx();
578        let result = is_valid_fn(
579            &[ValueWord::from_string(Arc::new(
580                "{not valid json".to_string(),
581            ))],
582            &ctx,
583        )
584        .unwrap();
585        assert_eq!(result.as_bool(), Some(false));
586    }
587
588    #[test]
589    fn test_json_is_valid_requires_string() {
590        let module = create_json_module();
591        let is_valid_fn = module.get_export("is_valid").unwrap();
592        let ctx = test_ctx();
593        let result = is_valid_fn(&[ValueWord::from_f64(42.0)], &ctx);
594        assert!(result.is_err());
595    }
596
597    #[test]
598    fn test_json_schemas() {
599        let module = create_json_module();
600
601        let parse_schema = module.get_schema("parse").unwrap();
602        assert_eq!(parse_schema.params.len(), 1);
603        assert_eq!(parse_schema.params[0].name, "text");
604        assert!(parse_schema.params[0].required);
605        assert_eq!(parse_schema.return_type.as_deref(), Some("Result<Json>"));
606
607        let stringify_schema = module.get_schema("stringify").unwrap();
608        assert_eq!(stringify_schema.params.len(), 2);
609        assert!(stringify_schema.params[0].required);
610        assert!(!stringify_schema.params[1].required);
611
612        let is_valid_schema = module.get_schema("is_valid").unwrap();
613        assert_eq!(is_valid_schema.params.len(), 1);
614        assert_eq!(is_valid_schema.return_type.as_deref(), Some("bool"));
615    }
616
617    #[test]
618    fn test_json_roundtrip_nested() {
619        let module = create_json_module();
620        let parse_fn = module.get_export("parse").unwrap();
621        let stringify_fn = module.get_export("stringify").unwrap();
622        let ctx = test_ctx();
623
624        let json_str = r#"{"name":"test","values":[1,2,3],"active":true,"meta":null}"#;
625        let parsed = parse_fn(
626            &[ValueWord::from_string(Arc::new(json_str.to_string()))],
627            &ctx,
628        )
629        .unwrap();
630        let inner = parsed.as_ok_inner().expect("should be Ok");
631
632        let re_stringified = stringify_fn(&[inner.clone()], &ctx).unwrap();
633        let re_str = re_stringified.as_ok_inner().expect("should be Ok");
634
635        // Re-parse to verify round-trip validity
636        let re_parsed = parse_fn(&[re_str.clone()], &ctx).unwrap();
637        assert!(re_parsed.as_ok_inner().is_some());
638    }
639
640    /// Test json_value_to_enum produces TypedObjects with correct variant IDs.
641    #[test]
642    fn test_json_value_to_enum_variants() {
643        use crate::type_schema::{EnumVariantInfo, TypeSchema};
644        // Register Json enum schema
645        let schema = TypeSchema::new_enum(
646            "Json",
647            vec![
648                EnumVariantInfo::new("Null", 0, 0),
649                EnumVariantInfo::new("Bool", 1, 1),
650                EnumVariantInfo::new("Number", 2, 1),
651                EnumVariantInfo::new("Str", 3, 1),
652                EnumVariantInfo::new("Array", 4, 1),
653                EnumVariantInfo::new("Object", 5, 1),
654            ],
655        );
656        let sid = schema.id as u64;
657
658        // Null
659        let null_nb = json_value_to_enum(serde_json::Value::Null, sid);
660        let (variant, _payload) = extract_enum_variant(&null_nb);
661        assert_eq!(variant, 0, "Null should be variant 0");
662
663        // Bool
664        let bool_nb = json_value_to_enum(serde_json::Value::Bool(true), sid);
665        let (variant, _payload) = extract_enum_variant(&bool_nb);
666        assert_eq!(variant, 1, "Bool should be variant 1");
667
668        // Number
669        let num_nb = json_value_to_enum(serde_json::json!(42.5), sid);
670        let (variant, _payload) = extract_enum_variant(&num_nb);
671        assert_eq!(variant, 2, "Number should be variant 2");
672
673        // String
674        let str_nb = json_value_to_enum(serde_json::json!("hello"), sid);
675        let (variant, _payload) = extract_enum_variant(&str_nb);
676        assert_eq!(variant, 3, "Str should be variant 3");
677
678        // Array
679        let arr_nb = json_value_to_enum(serde_json::json!([1, 2, 3]), sid);
680        let (variant, _payload) = extract_enum_variant(&arr_nb);
681        assert_eq!(variant, 4, "Array should be variant 4");
682
683        // Object
684        let obj_nb = json_value_to_enum(serde_json::json!({"a": 1}), sid);
685        let (variant, _payload) = extract_enum_variant(&obj_nb);
686        assert_eq!(variant, 5, "Object should be variant 5");
687    }
688
689    /// Test that __parse_typed uses @alias annotations.
690    #[test]
691    fn test_parse_typed_with_alias() {
692        use crate::type_schema::{FieldAnnotation, TypeSchemaBuilder};
693
694        let mut registry = crate::type_schema::TypeSchemaRegistry::new();
695        let mut schema = TypeSchemaBuilder::new("Trade")
696            .f64_field("close")
697            .f64_field("volume")
698            .build();
699
700        // Add @alias annotations manually
701        schema.fields[0].annotations.push(FieldAnnotation {
702            name: "alias".to_string(),
703            args: vec!["Close Price".to_string()],
704        });
705        schema.fields[1].annotations.push(FieldAnnotation {
706            name: "alias".to_string(),
707            args: vec!["vol.".to_string()],
708        });
709        let trade_id = schema.id;
710        registry.register(schema);
711
712        let module = create_json_module();
713        let parse_typed_fn = module.get_export("__parse_typed").unwrap();
714        let ctx = crate::module_exports::ModuleContext {
715            schemas: &registry,
716            invoke_callable: None,
717            raw_invoker: None,
718            function_hashes: None,
719            vm_state: None,
720            granted_permissions: None,
721            scope_constraints: None,
722            set_pending_resume: None,
723            set_pending_frame_resume: None,
724        };
725
726        let text = ValueWord::from_string(Arc::new(
727            r#"{"Close Price": 100.5, "vol.": 1000}"#.to_string(),
728        ));
729        let sid = ValueWord::from_f64(trade_id as f64);
730        let result = parse_typed_fn(&[text, sid], &ctx).unwrap();
731        let inner = result.as_ok_inner().expect("should be Ok");
732
733        // Verify it's a TypedObject
734        assert!(
735            inner.as_heap_ref().is_some(),
736            "typed parse result should be a heap value"
737        );
738    }
739
740    /// Extract variant_id from a Json enum TypedObject.
741    fn extract_enum_variant(nb: &ValueWord) -> (i64, Option<ValueWord>) {
742        use shape_value::heap_value::HeapValue;
743        if let Some(HeapValue::TypedObject {
744            slots, heap_mask, ..
745        }) = nb.as_heap_ref()
746        {
747            let variant_id = slots[0].as_i64();
748            let payload = if slots.len() > 1 {
749                // Only dereference as heap pointer if the heap_mask says slot 1 is a pointer
750                if heap_mask & (1u64 << 1) != 0 {
751                    Some(slots[1].as_heap_nb())
752                } else if slots[1].raw() == 0 && variant_id == 0 {
753                    // Null variant has no payload
754                    None
755                } else {
756                    // Non-heap payload (inline ValueWord) — reconstruct from raw bits
757                    // Safety: bits were stored by nb_to_slot from a valid inline ValueWord.
758                    Some(unsafe { ValueWord::clone_from_bits(slots[1].raw()) })
759                }
760            } else {
761                None
762            };
763            (variant_id, payload)
764        } else {
765            panic!("expected TypedObject, got: {:?}", nb.type_name())
766        }
767    }
768}