Skip to main content

runmat_runtime/builtins/io/json/
jsondecode.rs

1//! MATLAB-compatible `jsondecode` builtin for deserialising JSON text into RunMat values.
2
3use once_cell::sync::Lazy;
4use runmat_builtins::{
5    CellArray, CharArray, LogicalArray, StringArray, StructValue, Tensor, Value,
6};
7use runmat_macros::runtime_builtin;
8use serde_json::Value as JsonValue;
9
10use crate::builtins::common::spec::{
11    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12    ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
15
16const INPUT_TYPE_ERROR: &str = "jsondecode: JSON text must be a character vector or string scalar";
17const PARSE_ERROR_PREFIX: &str = "jsondecode: invalid JSON text";
18
19#[allow(clippy::too_many_lines)]
20#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::json::jsondecode")]
21pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
22    name: "jsondecode",
23    op_kind: GpuOpKind::Custom("parse"),
24    supported_precisions: &[],
25    broadcast: BroadcastSemantics::None,
26    provider_hooks: &[],
27    constant_strategy: ConstantStrategy::InlineLiteral,
28    residency: ResidencyPolicy::GatherImmediately,
29    nan_mode: ReductionNaN::Include,
30    two_pass_threshold: None,
31    workgroup_size: None,
32    accepts_nan_mode: false,
33    notes: "No GPU kernels: jsondecode gathers gpuArray input to host memory before parsing the JSON text.",
34};
35
36fn jsondecode_error(message: impl Into<String>) -> RuntimeError {
37    build_runtime_error(message)
38        .with_builtin("jsondecode")
39        .build()
40}
41
42fn jsondecode_flow_with_context(err: RuntimeError) -> RuntimeError {
43    let mut builder = build_runtime_error(err.message().to_string()).with_builtin("jsondecode");
44    if let Some(identifier) = err.identifier() {
45        builder = builder.with_identifier(identifier.to_string());
46    }
47    builder.with_source(err).build()
48}
49
50#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::json::jsondecode")]
51pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
52    name: "jsondecode",
53    shape: ShapeRequirements::Any,
54    constant_strategy: ConstantStrategy::InlineLiteral,
55    elementwise: None,
56    reduction: None,
57    emits_nan: false,
58    notes: "jsondecode is a residency sink; it always runs on the CPU and breaks fusion graphs.",
59};
60
61#[runtime_builtin(
62    name = "jsondecode",
63    category = "io/json",
64    summary = "Parse UTF-8 JSON text into MATLAB-compatible RunMat values.",
65    keywords = "jsondecode,json,parse json,struct,gpu",
66    accel = "sink",
67    type_resolver(crate::builtins::io::type_resolvers::jsondecode_type),
68    builtin_path = "crate::builtins::io::json::jsondecode"
69)]
70async fn jsondecode_builtin(text: Value) -> crate::BuiltinResult<Value> {
71    let gathered = gather_if_needed_async(&text)
72        .await
73        .map_err(jsondecode_flow_with_context)?;
74    let source = extract_text(gathered)?;
75    let parsed: JsonValue = serde_json::from_str(&source).map_err(|err| {
76        build_runtime_error(format!("{PARSE_ERROR_PREFIX} ({err})"))
77            .with_builtin("jsondecode")
78            .with_source(err)
79            .build()
80    })?;
81    value_from_json(&parsed)
82}
83
84pub(crate) fn decode_json_text(text: &str) -> BuiltinResult<Value> {
85    let parsed: JsonValue = serde_json::from_str(text).map_err(|err| {
86        build_runtime_error(format!("{PARSE_ERROR_PREFIX} ({err})"))
87            .with_builtin("jsondecode")
88            .with_source(err)
89            .build()
90    })?;
91    value_from_json(&parsed)
92}
93
94fn extract_text(value: Value) -> BuiltinResult<String> {
95    match value {
96        Value::CharArray(array) => {
97            if array.rows > 1 {
98                return Err(jsondecode_error(INPUT_TYPE_ERROR));
99            }
100            Ok(array.data.into_iter().collect::<String>())
101        }
102        Value::String(s) => Ok(s),
103        Value::StringArray(sa) => {
104            if sa.data.len() == 1 {
105                Ok(sa.data[0].clone())
106            } else {
107                Err(jsondecode_error(INPUT_TYPE_ERROR))
108            }
109        }
110        _other => Err(jsondecode_error(INPUT_TYPE_ERROR)),
111    }
112}
113
114fn value_from_json(value: &JsonValue) -> BuiltinResult<Value> {
115    match value {
116        JsonValue::Null => empty_double(),
117        JsonValue::Bool(b) => Ok(Value::Bool(*b)),
118        JsonValue::Number(num) => parse_json_number(num).map(Value::Num),
119        JsonValue::String(s) => {
120            let char_array = CharArray::new_row(s);
121            Ok(Value::CharArray(char_array))
122        }
123        JsonValue::Array(arr) => decode_json_array(arr),
124        JsonValue::Object(map) => decode_json_object(map),
125    }
126}
127
128fn decode_json_object(map: &serde_json::Map<String, JsonValue>) -> BuiltinResult<Value> {
129    let mut struct_value = StructValue::new();
130    for (key, val) in map {
131        struct_value
132            .fields
133            .insert(key.clone(), value_from_json(val)?);
134    }
135    Ok(Value::Struct(struct_value))
136}
137
138fn parse_json_number(number: &serde_json::Number) -> BuiltinResult<f64> {
139    if let Some(value) = number.as_f64() {
140        return Ok(value);
141    }
142    let text = number.to_string();
143    match text.parse::<f64>() {
144        Ok(value) => {
145            if value.is_nan() {
146                Err(jsondecode_error(format!(
147                    "{PARSE_ERROR_PREFIX}: unsupported numeric literal ({text})"
148                )))
149            } else {
150                Ok(value)
151            }
152        }
153        Err(_) => {
154            let display = if text.len() > 64 {
155                format!("{}...", &text[..64])
156            } else {
157                text
158            };
159            Err(jsondecode_error(format!(
160                "{PARSE_ERROR_PREFIX}: numeric literal out of range ({display})"
161            )))
162        }
163    }
164}
165
166fn decode_json_array(values: &[JsonValue]) -> BuiltinResult<Value> {
167    if values.is_empty() {
168        let tensor = Tensor::new(Vec::new(), vec![0, 0])
169            .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
170        return Ok(Value::Tensor(tensor));
171    }
172
173    if let Some(numeric) = parse_numeric_array(values)? {
174        let tensor = Tensor::new(numeric.data, numeric.shape)
175            .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
176        return Ok(Value::Tensor(tensor));
177    }
178
179    if let Some(logical) = parse_logical_array(values) {
180        let array = LogicalArray::new(logical.data, logical.shape)
181            .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
182        return Ok(Value::LogicalArray(array));
183    }
184
185    if let Some(strings) = parse_string_array(values) {
186        let array = StringArray::new(strings.data, strings.shape)
187            .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
188        return Ok(Value::StringArray(array));
189    }
190
191    if let Some(cell) = parse_rectangular_cell_array(values)? {
192        return Ok(cell);
193    }
194
195    let mut elements = Vec::with_capacity(values.len());
196    for element in values {
197        elements.push(value_from_json(element)?);
198    }
199    cell_row(elements)
200}
201
202fn empty_double() -> BuiltinResult<Value> {
203    static EMPTY_DOUBLE: Lazy<Option<Value>> =
204        Lazy::new(|| Tensor::new(Vec::new(), vec![0, 0]).map(Value::Tensor).ok());
205    if let Some(value) = EMPTY_DOUBLE.as_ref() {
206        return Ok(value.clone());
207    }
208    Tensor::new(Vec::new(), vec![0, 0])
209        .map(Value::Tensor)
210        .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))
211}
212
213fn cell_matrix(elements: Vec<Value>, rows: usize, cols: usize) -> BuiltinResult<Value> {
214    let cell = CellArray::new(elements, rows, cols)
215        .map_err(|e| jsondecode_error(format!("jsondecode: {e}")))?;
216    Ok(Value::Cell(cell))
217}
218
219fn cell_row(elements: Vec<Value>) -> BuiltinResult<Value> {
220    let cols = elements.len();
221    cell_matrix(elements, 1, cols)
222}
223
224struct NumericTensor {
225    data: Vec<f64>,
226    shape: Vec<usize>,
227}
228
229fn parse_numeric_array(values: &[JsonValue]) -> BuiltinResult<Option<NumericTensor>> {
230    if values.is_empty() {
231        return Ok(None);
232    }
233
234    if values.iter().all(|v| v.is_number()) {
235        let mut data = Vec::with_capacity(values.len());
236        for value in values {
237            if let JsonValue::Number(number) = value {
238                data.push(parse_json_number(number)?);
239            }
240        }
241        return Ok(Some(NumericTensor {
242            data,
243            shape: vec![values.len()],
244        }));
245    }
246
247    if values.iter().all(|v| v.is_array()) {
248        let mut children = Vec::with_capacity(values.len());
249        for value in values {
250            let Some(child_values) = value.as_array() else {
251                return Ok(None);
252            };
253            let Some(child) = parse_numeric_array(child_values)? else {
254                return Ok(None);
255            };
256            children.push(child);
257        }
258        if children.is_empty() {
259            return Ok(None);
260        }
261        let first_shape = children[0].shape.clone();
262        if !children.iter().all(|child| child.shape == first_shape) {
263            return Ok(None);
264        }
265        let mut shape = Vec::with_capacity(first_shape.len() + 1);
266        shape.push(children.len());
267        shape.extend(first_shape.clone());
268
269        let total: usize = shape.iter().product();
270        let rows = shape[0];
271        if rows == 0 {
272            return Ok(None);
273        }
274        let inner = total / rows;
275        let mut data = vec![0.0; total];
276        for (row, child) in children.into_iter().enumerate() {
277            if child.data.len() != inner {
278                return Ok(None);
279            }
280            for (idx, value) in child.data.into_iter().enumerate() {
281                let offset = row + rows * idx;
282                data[offset] = value;
283            }
284        }
285        return Ok(Some(NumericTensor { data, shape }));
286    }
287
288    Ok(None)
289}
290
291struct LogicalTensor {
292    data: Vec<u8>,
293    shape: Vec<usize>,
294}
295
296fn parse_logical_array(values: &[JsonValue]) -> Option<LogicalTensor> {
297    if values.is_empty() {
298        return None;
299    }
300
301    if values.iter().all(|v| v.is_boolean()) {
302        let mut data = Vec::with_capacity(values.len());
303        for value in values {
304            data.push(if value.as_bool()? { 1 } else { 0 });
305        }
306        return Some(LogicalTensor {
307            data,
308            shape: vec![values.len()],
309        });
310    }
311
312    if values.iter().all(|v| v.is_array()) {
313        let mut children = Vec::with_capacity(values.len());
314        for value in values {
315            let child = parse_logical_array(value.as_array()?)?;
316            children.push(child);
317        }
318        if children.is_empty() {
319            return None;
320        }
321        let first_shape = children[0].shape.clone();
322        if !children.iter().all(|child| child.shape == first_shape) {
323            return None;
324        }
325        let mut shape = Vec::with_capacity(first_shape.len() + 1);
326        shape.push(children.len());
327        shape.extend(first_shape.clone());
328
329        let total: usize = shape.iter().product();
330        let rows = shape[0];
331        if rows == 0 {
332            return None;
333        }
334        let inner = total / rows;
335        let mut data = vec![0u8; total];
336        for (row, child) in children.into_iter().enumerate() {
337            if child.data.len() != inner {
338                return None;
339            }
340            for (idx, value) in child.data.into_iter().enumerate() {
341                let offset = row + rows * idx;
342                data[offset] = value;
343            }
344        }
345        return Some(LogicalTensor { data, shape });
346    }
347
348    None
349}
350
351struct StringTensor {
352    data: Vec<String>,
353    shape: Vec<usize>,
354}
355
356fn parse_string_array(values: &[JsonValue]) -> Option<StringTensor> {
357    if values.is_empty() {
358        return None;
359    }
360
361    if values.iter().all(|v| v.is_string()) {
362        let mut data = Vec::with_capacity(values.len());
363        for value in values {
364            data.push(value.as_str()?.to_string());
365        }
366        return Some(StringTensor {
367            data,
368            shape: vec![values.len()],
369        });
370    }
371
372    if values.iter().all(|v| v.is_array()) {
373        let mut children = Vec::with_capacity(values.len());
374        for value in values {
375            let child = parse_string_array(value.as_array()?)?;
376            children.push(child);
377        }
378        if children.is_empty() {
379            return None;
380        }
381        let first_shape = children[0].shape.clone();
382        if !children.iter().all(|child| child.shape == first_shape) {
383            return None;
384        }
385        let mut shape = Vec::with_capacity(first_shape.len() + 1);
386        shape.push(children.len());
387        shape.extend(first_shape.clone());
388
389        let total: usize = shape.iter().product();
390        let rows = shape[0];
391        if rows == 0 {
392            return None;
393        }
394        let inner = total / rows;
395        let mut data = vec![String::new(); total];
396        for (row, mut child) in children.into_iter().enumerate() {
397            if child.data.len() != inner {
398                return None;
399            }
400            for (idx, value) in child.data.drain(..).enumerate() {
401                let offset = row + rows * idx;
402                data[offset] = value;
403            }
404        }
405        return Some(StringTensor { data, shape });
406    }
407
408    None
409}
410
411fn parse_rectangular_cell_array(values: &[JsonValue]) -> BuiltinResult<Option<Value>> {
412    if values.is_empty() || !values.iter().all(|v| v.is_array()) {
413        return Ok(None);
414    }
415
416    let mut expected_len: Option<usize> = None;
417    for value in values {
418        let arr = value.as_array().ok_or_else(|| {
419            // Should be unreachable due to the `all(|v| v.is_array())` guard above.
420            jsondecode_error("jsondecode: inconsistent array state")
421        })?;
422        match expected_len {
423            Some(len) if arr.len() != len => return Ok(None),
424            None => expected_len = Some(arr.len()),
425            _ => {}
426        }
427    }
428
429    let cols = expected_len.unwrap_or(0);
430    let rows = values.len();
431
432    let mut elements = Vec::with_capacity(rows.saturating_mul(cols));
433    if cols > 0 {
434        for value in values {
435            let arr = value.as_array().expect("validated array value");
436            for element in arr {
437                elements.push(value_from_json(element)?);
438            }
439        }
440    }
441
442    let cell = cell_matrix(elements, rows, cols)?;
443    Ok(Some(cell))
444}
445
446#[cfg(test)]
447pub(crate) mod tests {
448    use super::*;
449    use futures::executor::block_on;
450    use runmat_builtins::{IntValue, Tensor};
451
452    use crate::RuntimeError;
453
454    fn char_row(text: &str) -> Value {
455        Value::CharArray(CharArray::new_row(text))
456    }
457
458    fn error_message(err: RuntimeError) -> String {
459        err.message().to_string()
460    }
461
462    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
463    #[test]
464    fn jsondecode_scalar_number() {
465        let result = block_on(jsondecode_builtin(char_row("42"))).expect("jsondecode");
466        assert_eq!(result, Value::Num(42.0));
467    }
468
469    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
470    #[test]
471    fn jsondecode_boolean_array() {
472        let result =
473            block_on(jsondecode_builtin(char_row("[true,false,true]"))).expect("jsondecode");
474        match result {
475            Value::LogicalArray(array) => {
476                assert_eq!(array.shape, vec![3]);
477                assert_eq!(array.data, vec![1, 0, 1]);
478            }
479            other => panic!("expected logical array, got {:?}", other),
480        }
481    }
482
483    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
484    #[test]
485    fn jsondecode_matrix_to_tensor() {
486        let result =
487            block_on(jsondecode_builtin(char_row("[[1,2,3],[4,5,6]]"))).expect("jsondecode matrix");
488        match result {
489            Value::Tensor(tensor) => {
490                assert_eq!(tensor.shape, vec![2, 3]);
491                assert_eq!(tensor.data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
492            }
493            other => panic!("expected tensor, got {:?}", other),
494        }
495    }
496
497    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
498    #[test]
499    fn jsondecode_numeric_tensor_3d() {
500        let json = "[[[1,2],[3,4]],[[5,6],[7,8]]]";
501        let result = block_on(jsondecode_builtin(char_row(json))).expect("jsondecode 3d tensor");
502        match result {
503            Value::Tensor(tensor) => {
504                assert_eq!(tensor.shape, vec![2, 2, 2]);
505                assert_eq!(tensor.data, vec![1.0, 5.0, 3.0, 7.0, 2.0, 6.0, 4.0, 8.0]);
506            }
507            other => panic!("expected tensor, got {:?}", other),
508        }
509    }
510
511    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
512    #[test]
513    fn jsondecode_numeric_singleton_array_retains_tensor() {
514        let result = block_on(jsondecode_builtin(char_row("[42]")))
515            .expect("jsondecode singleton numeric array");
516        match result {
517            Value::Tensor(tensor) => {
518                assert_eq!(tensor.shape, vec![1]);
519                assert_eq!(tensor.rows(), 1);
520                assert_eq!(tensor.cols(), 1);
521                assert_eq!(tensor.data, vec![42.0]);
522            }
523            other => panic!("expected tensor, got {:?}", other),
524        }
525    }
526
527    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
528    #[test]
529    fn jsondecode_object_to_struct() {
530        let result = block_on(jsondecode_builtin(char_row(
531            "{\"name\":\"RunMat\",\"year\":2025}",
532        )))
533        .expect("jsondecode struct");
534        match result {
535            Value::Struct(struct_value) => {
536                assert_eq!(
537                    struct_value.fields.get("name"),
538                    Some(&Value::CharArray(CharArray::new_row("RunMat")))
539                );
540                match struct_value.fields.get("year") {
541                    Some(Value::Num(year)) => assert_eq!(*year, 2025.0),
542                    Some(Value::Int(IntValue::I32(year))) => assert_eq!(*year, 2025),
543                    other => panic!("unexpected year field {other:?}"),
544                }
545            }
546            other => panic!("expected struct, got {:?}", other),
547        }
548    }
549
550    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
551    #[test]
552    fn jsondecode_string_array() {
553        let result = block_on(jsondecode_builtin(char_row(
554            "[\"alpha\",\"beta\",\"gamma\"]",
555        )))
556        .expect("jsondecode string array");
557        match result {
558            Value::StringArray(array) => {
559                assert_eq!(array.shape, vec![3]);
560                assert_eq!(array.rows, 1);
561                assert_eq!(array.cols, 3);
562                assert_eq!(
563                    array.data,
564                    vec![
565                        String::from("alpha"),
566                        String::from("beta"),
567                        String::from("gamma"),
568                    ]
569                );
570            }
571            other => panic!("expected string array, got {:?}", other),
572        }
573    }
574
575    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
576    #[test]
577    fn jsondecode_mixed_array_returns_cell() {
578        let result = block_on(jsondecode_builtin(char_row("[\"RunMat\",42,true]")))
579            .expect("jsondecode mixed");
580        match result {
581            Value::Cell(cell) => {
582                assert_eq!(cell.rows, 1);
583                assert_eq!(cell.cols, 3);
584                let first = cell.get(0, 0).expect("cell");
585                assert_eq!(first, Value::CharArray(CharArray::new_row("RunMat")));
586                let second = cell.get(0, 1).expect("cell");
587                assert_eq!(second, Value::Num(42.0));
588                let third = cell.get(0, 2).expect("cell");
589                assert_eq!(third, Value::Bool(true));
590            }
591            other => panic!("expected cell array, got {:?}", other),
592        }
593    }
594
595    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
596    #[test]
597    fn jsondecode_rectangular_cell_array_preserves_layout() {
598        let text = "[[1,true],[false,null]]";
599        let result = block_on(jsondecode_builtin(char_row(text)))
600            .expect("jsondecode rectangular heterogeneous array");
601        match result {
602            Value::Cell(cell) => {
603                assert_eq!(cell.rows, 2);
604                assert_eq!(cell.cols, 2);
605                assert_eq!(cell.get(0, 0).unwrap(), Value::Num(1.0));
606                assert_eq!(cell.get(0, 1).unwrap(), Value::Bool(true));
607                assert_eq!(cell.get(1, 0).unwrap(), Value::Bool(false));
608                match cell.get(1, 1).unwrap() {
609                    Value::Tensor(t) => {
610                        assert_eq!(t.shape, vec![0, 0]);
611                        assert!(t.data.is_empty());
612                    }
613                    other => panic!("expected empty tensor, got {:?}", other),
614                }
615            }
616            other => panic!("expected cell array, got {:?}", other),
617        }
618    }
619
620    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
621    #[test]
622    fn jsondecode_array_of_objects_returns_cell() {
623        let text = "[{\"id\":1,\"name\":\"Ada\"},{\"id\":2,\"name\":\"Charles\"}]";
624        let result = block_on(jsondecode_builtin(char_row(text))).expect("jsondecode object array");
625        match result {
626            Value::Cell(cell) => {
627                assert_eq!(cell.rows, 1);
628                assert_eq!(cell.cols, 2);
629
630                let first = cell.get(0, 0).expect("first struct");
631                match first {
632                    Value::Struct(struct_value) => {
633                        assert_eq!(struct_value.fields.get("id"), Some(&Value::Num(1.0)));
634                        assert_eq!(
635                            struct_value.fields.get("name"),
636                            Some(&Value::CharArray(CharArray::new_row("Ada")))
637                        );
638                    }
639                    other => panic!("expected struct, got {:?}", other),
640                }
641
642                let second = cell.get(0, 1).expect("second struct");
643                match second {
644                    Value::Struct(struct_value) => {
645                        assert_eq!(struct_value.fields.get("id"), Some(&Value::Num(2.0)));
646                        assert_eq!(
647                            struct_value.fields.get("name"),
648                            Some(&Value::CharArray(CharArray::new_row("Charles")))
649                        );
650                    }
651                    other => panic!("expected struct, got {:?}", other),
652                }
653            }
654            other => panic!("expected cell array, got {:?}", other),
655        }
656    }
657
658    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
659    #[test]
660    fn jsondecode_null_returns_empty_double() {
661        let result = block_on(jsondecode_builtin(char_row("null"))).expect("jsondecode null");
662        match result {
663            Value::Tensor(tensor) => {
664                assert_eq!(tensor.shape, vec![0, 0]);
665                assert!(tensor.data.is_empty());
666            }
667            other => panic!("expected empty tensor, got {:?}", other),
668        }
669    }
670
671    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
672    #[test]
673    fn jsondecode_invalid_text_reports_error() {
674        let err =
675            block_on(jsondecode_builtin(char_row("{not json}"))).expect_err("expected failure");
676        let err = error_message(err);
677        assert!(
678            err.starts_with(PARSE_ERROR_PREFIX),
679            "unexpected error message: {err}"
680        );
681    }
682
683    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
684    #[test]
685    fn jsondecode_rejects_multirow_char_input() {
686        let chars = CharArray::new(vec!['a', 'b', 'c', 'd'], 2, 2).expect("char array");
687        let err =
688            block_on(jsondecode_builtin(Value::CharArray(chars))).expect_err("expected type error");
689        assert_eq!(error_message(err), INPUT_TYPE_ERROR);
690    }
691
692    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
693    #[test]
694    fn jsondecode_accepts_string_input() {
695        let result =
696            block_on(jsondecode_builtin(Value::String("[1,2]".to_string()))).expect("jsondecode");
697        match result {
698            Value::Tensor(tensor) => {
699                assert_eq!(tensor.shape, vec![2]);
700                assert_eq!(tensor.data, vec![1.0, 2.0]);
701            }
702            other => panic!("expected tensor, got {:?}", other),
703        }
704    }
705
706    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
707    #[test]
708    fn jsondecode_accepts_string_array_scalar_input() {
709        let array = StringArray::new(vec!["[1,2]".to_string()], vec![1, 1]).expect("string scalar");
710        let result = block_on(jsondecode_builtin(Value::StringArray(array))).expect("jsondecode");
711        match result {
712            Value::Tensor(tensor) => {
713                assert_eq!(tensor.shape, vec![2]);
714                assert_eq!(tensor.data, vec![1.0, 2.0]);
715            }
716            other => panic!("expected tensor, got {:?}", other),
717        }
718    }
719
720    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721    #[test]
722    fn jsondecode_round_trip_with_jsonencode() {
723        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).expect("tensor");
724        let encoded = block_on(crate::call_builtin_async(
725            "jsonencode",
726            &[Value::Tensor(tensor.clone())],
727        ))
728        .expect("encode");
729        let decoded = block_on(jsondecode_builtin(encoded)).expect("decode");
730        match decoded {
731            Value::Tensor(result) => {
732                assert_eq!(result.data.len(), tensor.data.len());
733                assert_eq!(result.data, tensor.data);
734            }
735            other => panic!("expected tensor, got {:?}", other),
736        }
737    }
738}