Skip to main content

soil_rt/json/
decode.rs

1//! Type-directed decode (impl plan §4 step 6).
2//!
3//! Parsing is borrowed from `serde_json` (into an order-preserving,
4//! duplicate-rejecting tree — micro-pin §8.7); the walk from tree to
5//! `Value` is owned here and is strict: unknown record fields, missing
6//! non-ignored fields, *present* `ignored` fields (§8.5), wrong numeric
7//! shapes, non-canonical base64 (§8.4), unknown tags, and duplicate map
8//! keys are all `DecodeError`s naming the JSON path. Recursion depth is
9//! bounded by `serde_json`'s parser limit, which applies while the tree
10//! is built.
11
12use std::str::FromStr;
13
14use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
15use base64::Engine;
16use num_bigint::BigInt;
17use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
18use serde::Deserialize;
19
20use crate::descriptor::{Strategy, TypeBody, TypeShape};
21use crate::error::{PanicKind, SoilError};
22use crate::value::{MapOrder, MapVal, Record, Ref, SumVal, Value};
23use crate::Runtime;
24
25/// Strict base64: standard alphabet, canonical padding required, no
26/// trailing bits (micro-pin §8.4).
27const BASE64: GeneralPurpose = GeneralPurpose::new(
28    &base64::alphabet::STANDARD,
29    GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::RequireCanonical),
30);
31
32/// Decode canonical (or canonically-equivalent) JSON into a value of
33/// the given shape — the runtime half of `parse` (design §3.7).
34pub fn decode(runtime: &Runtime, shape: &TypeShape, json: &str) -> Result<Value, SoilError> {
35    let tree: Tree = serde_json::from_str(json)
36        .map_err(|e| SoilError::new(PanicKind::DecodeError, format!("invalid JSON: {e}")))?;
37    decode_tree(runtime, shape, &tree, "$")
38}
39
40/// An order-preserving JSON tree that rejects duplicate object keys at
41/// parse time (`serde_json`'s own map type is last-wins).
42enum Tree {
43    Null,
44    Bool(bool),
45    Num(serde_json::Number),
46    Str(String),
47    Arr(Vec<Tree>),
48    Obj(Vec<(String, Tree)>),
49}
50
51impl Tree {
52    fn kind(&self) -> &'static str {
53        match self {
54            Tree::Null => "null",
55            Tree::Bool(_) => "boolean",
56            Tree::Num(_) => "number",
57            Tree::Str(_) => "string",
58            Tree::Arr(_) => "array",
59            Tree::Obj(_) => "object",
60        }
61    }
62}
63
64impl<'de> Deserialize<'de> for Tree {
65    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Tree, D::Error> {
66        struct TreeVisitor;
67
68        impl<'de> Visitor<'de> for TreeVisitor {
69            type Value = Tree;
70
71            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72                f.write_str("any JSON value")
73            }
74
75            fn visit_unit<E>(self) -> Result<Tree, E> {
76                Ok(Tree::Null)
77            }
78
79            fn visit_bool<E>(self, b: bool) -> Result<Tree, E> {
80                Ok(Tree::Bool(b))
81            }
82
83            fn visit_i64<E>(self, n: i64) -> Result<Tree, E> {
84                Ok(Tree::Num(n.into()))
85            }
86
87            fn visit_u64<E>(self, n: u64) -> Result<Tree, E> {
88                Ok(Tree::Num(n.into()))
89            }
90
91            fn visit_f64<E: de::Error>(self, n: f64) -> Result<Tree, E> {
92                serde_json::Number::from_f64(n)
93                    .map(Tree::Num)
94                    .ok_or_else(|| E::custom("non-finite number"))
95            }
96
97            fn visit_str<E>(self, s: &str) -> Result<Tree, E> {
98                Ok(Tree::Str(s.to_string()))
99            }
100
101            fn visit_string<E>(self, s: String) -> Result<Tree, E> {
102                Ok(Tree::Str(s))
103            }
104
105            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Tree, A::Error> {
106                let mut items = Vec::new();
107                while let Some(item) = seq.next_element()? {
108                    items.push(item);
109                }
110                Ok(Tree::Arr(items))
111            }
112
113            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Tree, A::Error> {
114                let mut entries: Vec<(String, Tree)> = Vec::new();
115                while let Some((key, value)) = map.next_entry::<String, Tree>()? {
116                    if entries.iter().any(|(k, _)| *k == key) {
117                        return Err(de::Error::custom(format!("duplicate object key {key:?}")));
118                    }
119                    entries.push((key, value));
120                }
121                Ok(Tree::Obj(entries))
122            }
123        }
124
125        deserializer.deserialize_any(TreeVisitor)
126    }
127}
128
129fn err(path: &str, message: impl std::fmt::Display) -> SoilError {
130    SoilError::new(PanicKind::DecodeError, format!("at {path}: {message}"))
131}
132
133fn decode_tree(
134    runtime: &Runtime,
135    shape: &TypeShape,
136    tree: &Tree,
137    path: &str,
138) -> Result<Value, SoilError> {
139    match shape {
140        TypeShape::I64 => Ok(Value::I64(decode_int(tree, path, "I64")?)),
141        TypeShape::U64 => Ok(Value::U64(decode_int(tree, path, "U64")?)),
142        TypeShape::I32 => Ok(Value::I32(decode_int(tree, path, "I32")?)),
143        TypeShape::U32 => Ok(Value::U32(decode_int(tree, path, "U32")?)),
144        TypeShape::I16 => Ok(Value::I16(decode_int(tree, path, "I16")?)),
145        TypeShape::U16 => Ok(Value::U16(decode_int(tree, path, "U16")?)),
146        TypeShape::I8 => Ok(Value::I8(decode_int(tree, path, "I8")?)),
147        TypeShape::U8 => Ok(Value::U8(decode_int(tree, path, "U8")?)),
148        TypeShape::BigInt => match tree {
149            Tree::Num(n) => {
150                if let Some(i) = n.as_i64() {
151                    Ok(Value::BigInt(Ref::new(BigInt::from(i))))
152                } else if let Some(u) = n.as_u64() {
153                    Ok(Value::BigInt(Ref::new(BigInt::from(u))))
154                } else {
155                    Err(err(path, "BigInt number must be an exact integer"))
156                }
157            }
158            Tree::Str(s) => BigInt::from_str(s)
159                .map(|b| Value::BigInt(Ref::new(b)))
160                .map_err(|_| err(path, format!("invalid BigInt string {s:?}"))),
161            other => Err(err(
162                path,
163                format!("expected BigInt, found {}", other.kind()),
164            )),
165        },
166        TypeShape::F64 => match tree {
167            Tree::Num(n) => Ok(Value::F64(n.as_f64().expect("finite by construction"))),
168            Tree::Str(s) => match s.as_str() {
169                "NaN" => Ok(Value::F64(f64::NAN)),
170                "Inf" => Ok(Value::F64(f64::INFINITY)),
171                "-Inf" => Ok(Value::F64(f64::NEG_INFINITY)),
172                _ => Err(err(path, format!("invalid F64 string {s:?}"))),
173            },
174            other => Err(err(path, format!("expected F64, found {}", other.kind()))),
175        },
176        TypeShape::Utf8 => match tree {
177            Tree::Str(s) => Ok(Value::Utf8(Ref::from(s.as_str()))),
178            other => Err(err(
179                path,
180                format!("expected string, found {}", other.kind()),
181            )),
182        },
183        TypeShape::Bytes => match tree {
184            Tree::Str(s) => BASE64
185                .decode(s)
186                .map(|b| Value::Bytes(Ref::from(b)))
187                .map_err(|e| err(path, format!("invalid base64: {e}"))),
188            other => Err(err(
189                path,
190                format!("expected base64 string, found {}", other.kind()),
191            )),
192        },
193        TypeShape::Unit => match tree {
194            Tree::Null => Ok(Value::Unit),
195            other => Err(err(path, format!("expected null, found {}", other.kind()))),
196        },
197        TypeShape::List(elem) => match tree {
198            Tree::Arr(items) => {
199                let mut values = Vec::with_capacity(items.len());
200                for (i, item) in items.iter().enumerate() {
201                    values.push(decode_tree(runtime, elem, item, &format!("{path}[{i}]"))?);
202                }
203                Ok(Value::List(Ref::new(values)))
204            }
205            other => Err(err(path, format!("expected array, found {}", other.kind()))),
206        },
207        TypeShape::Map(key_shape, value_shape) => match tree {
208            Tree::Arr(items) => {
209                // Decoded maps carry the derived structural order: the
210                // JSON boundary has no program closure to hand them.
211                let mut map = MapVal::new(MapOrder::Structural);
212                for (i, item) in items.iter().enumerate() {
213                    let entry_path = format!("{path}[{i}]");
214                    let Tree::Obj(entries) = item else {
215                        return Err(err(&entry_path, "map entry must be an object"));
216                    };
217                    let mut key = None;
218                    let mut value = None;
219                    for (k, v) in entries {
220                        match k.as_str() {
221                            "key" => key = Some(v),
222                            "value" => value = Some(v),
223                            other => {
224                                return Err(err(
225                                    &entry_path,
226                                    format!("unexpected map-entry key {other:?}"),
227                                ));
228                            }
229                        }
230                    }
231                    let (Some(key), Some(value)) = (key, value) else {
232                        return Err(err(&entry_path, "map entry needs \"key\" and \"value\""));
233                    };
234                    let key = decode_tree(runtime, key_shape, key, &format!("{entry_path}.key"))?;
235                    let value =
236                        decode_tree(runtime, value_shape, value, &format!("{entry_path}.value"))?;
237                    if map.get(runtime, &key)?.is_some() {
238                        return Err(err(&entry_path, "duplicate map key"));
239                    }
240                    map.insert(runtime, key, value)?;
241                }
242                Ok(Value::Map(Ref::new(map)))
243            }
244            other => Err(err(path, format!("expected array, found {}", other.kind()))),
245        },
246        TypeShape::Closure => Err(err(path, "functions have no JSON encoding")),
247        TypeShape::Named(id) => {
248            if *id == runtime.registry.bool_id() {
249                return match tree {
250                    Tree::Bool(b) => Ok(Value::Sum(Ref::new(SumVal {
251                        type_id: *id,
252                        variant: if *b { 0 } else { 1 },
253                        payload: None,
254                    }))),
255                    other => Err(err(
256                        path,
257                        format!("expected boolean, found {}", other.kind()),
258                    )),
259                };
260            }
261            let desc = runtime.registry.get(*id)?;
262            if desc.strategy == Strategy::Opaque {
263                return Err(err(
264                    path,
265                    format!("type {} is opaque and cannot be decoded", desc.name),
266                ));
267            }
268            match &desc.body {
269                TypeBody::Record(fields) => {
270                    let Tree::Obj(entries) = tree else {
271                        return Err(err(path, format!("expected object, found {}", tree.kind())));
272                    };
273                    for (key, _) in entries {
274                        match fields.iter().find(|f| f.name == *key) {
275                            None => {
276                                return Err(err(
277                                    path,
278                                    format!("unknown field {key:?} on {}", desc.name),
279                                ));
280                            }
281                            Some(f) if f.ignored.is_some() => {
282                                // Micro-pin §8.5: encode omits ignored
283                                // fields, so decode rejects them — one
284                                // canonical form in both directions.
285                                return Err(err(
286                                    path,
287                                    format!(
288                                        "field {key:?} on {} is ignored and must be omitted",
289                                        desc.name
290                                    ),
291                                ));
292                            }
293                            Some(_) => {}
294                        }
295                    }
296                    let mut decoded = Vec::with_capacity(fields.len());
297                    for field in fields {
298                        if field.ignored.is_some() {
299                            decoded.push(None);
300                            continue;
301                        }
302                        let Some((_, subtree)) = entries.iter().find(|(k, _)| *k == field.name)
303                        else {
304                            return Err(err(
305                                path,
306                                format!("missing field {:?} on {}", field.name, desc.name),
307                            ));
308                        };
309                        decoded.push(Some(decode_tree(
310                            runtime,
311                            &field.shape,
312                            subtree,
313                            &format!("{path}.{}", field.name),
314                        )?));
315                    }
316                    // Refill ignored fields from their default thunks,
317                    // passing the non-ignored fields in declaration
318                    // order (micro-pin §8.6).
319                    let thunk_args: Vec<Value> = decoded.iter().flatten().cloned().collect();
320                    let mut values = Vec::with_capacity(fields.len());
321                    for (field, slot) in fields.iter().zip(decoded) {
322                        match slot {
323                            Some(value) => values.push(value),
324                            None => {
325                                let default = field
326                                    .ignored
327                                    .as_ref()
328                                    .expect("slot is empty only for ignored fields");
329                                values.push(default.call(&thunk_args)?);
330                            }
331                        }
332                    }
333                    Ok(Value::Record(Ref::new(Record {
334                        type_id: *id,
335                        fields: values.into_boxed_slice(),
336                    })))
337                }
338                TypeBody::Sum(variants) => {
339                    let Tree::Obj(entries) = tree else {
340                        return Err(err(path, format!("expected object, found {}", tree.kind())));
341                    };
342                    let mut tag = None;
343                    let mut payload_tree = None;
344                    for (key, value) in entries {
345                        match key.as_str() {
346                            "tag" => tag = Some(value),
347                            "value" => payload_tree = Some(value),
348                            other => {
349                                return Err(err(path, format!("unexpected key {other:?} in sum")));
350                            }
351                        }
352                    }
353                    let Some(Tree::Str(tag)) = tag else {
354                        return Err(err(path, "sum needs a string \"tag\""));
355                    };
356                    let Some(variant_index) = variants.iter().position(|v| v.name == *tag) else {
357                        return Err(err(
358                            path,
359                            format!("unknown variant {tag:?} of {}", desc.name),
360                        ));
361                    };
362                    let variant = &variants[variant_index];
363                    let payload = match (&variant.payload, payload_tree) {
364                        (Some(shape), Some(subtree)) => Some(decode_tree(
365                            runtime,
366                            shape,
367                            subtree,
368                            &format!("{path}.value"),
369                        )?),
370                        (None, None) => None,
371                        (Some(_), None) => {
372                            return Err(err(
373                                path,
374                                format!("variant {tag} of {} needs a \"value\"", desc.name),
375                            ));
376                        }
377                        (None, Some(_)) => {
378                            return Err(err(
379                                path,
380                                format!("variant {tag} of {} takes no \"value\"", desc.name),
381                            ));
382                        }
383                    };
384                    Ok(Value::Sum(Ref::new(SumVal {
385                        type_id: *id,
386                        variant: u32::try_from(variant_index).expect("validated at registration"),
387                        payload,
388                    })))
389                }
390                TypeBody::Opaque => unreachable!("opaque body implies opaque strategy"),
391            }
392        }
393    }
394}
395
396/// Decode an integer of any fixed width: the number must be an exact
397/// integer (no `1.0`, no floats) and in range for the target.
398fn decode_int<T: TryFrom<i128>>(tree: &Tree, path: &str, width: &str) -> Result<T, SoilError> {
399    let Tree::Num(n) = tree else {
400        return Err(err(
401            path,
402            format!("expected {width}, found {}", tree.kind()),
403        ));
404    };
405    let wide: i128 = if let Some(i) = n.as_i64() {
406        i128::from(i)
407    } else if let Some(u) = n.as_u64() {
408        i128::from(u)
409    } else {
410        return Err(err(path, format!("{width} must be an exact integer")));
411    };
412    T::try_from(wide).map_err(|_| err(path, format!("{n} out of range for {width}")))
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::descriptor::{FieldDesc, TypeDesc, VariantDesc};
419    use crate::json::encode;
420    use crate::ops;
421    use crate::value::Closure;
422
423    fn doc_runtime() -> (Runtime, crate::descriptor::TypeId) {
424        let mut rt = Runtime::new();
425        let id = rt
426            .registry
427            .register(TypeDesc {
428                name: "Doc".to_string(),
429                strategy: Strategy::Structural,
430                body: TypeBody::Record(vec![
431                    FieldDesc {
432                        name: "text".to_string(),
433                        shape: TypeShape::Utf8,
434                        ignored: None,
435                    },
436                    FieldDesc {
437                        name: "cached_len".to_string(),
438                        shape: TypeShape::U64,
439                        ignored: Some(crate::descriptor::IgnoredDefault::Native(Closure::native(
440                            |args| match &args[0] {
441                                Value::Utf8(s) => Ok(Value::U64(s.len() as u64)),
442                                _ => unreachable!(),
443                            },
444                        ))),
445                    },
446                    FieldDesc {
447                        name: "id".to_string(),
448                        shape: TypeShape::I64,
449                        ignored: None,
450                    },
451                ]),
452            })
453            .unwrap();
454        (rt, id)
455    }
456
457    #[test]
458    fn scalar_round_trips_and_strictness() {
459        let rt = Runtime::new();
460        assert!(matches!(
461            decode(&rt, &TypeShape::I64, "-42").unwrap(),
462            Value::I64(-42)
463        ));
464        assert!(decode(&rt, &TypeShape::I64, "1.5").is_err());
465        assert!(decode(&rt, &TypeShape::I64, "1.0").is_err());
466        assert!(decode(&rt, &TypeShape::I64, "\"1\"").is_err());
467        assert!(decode(&rt, &TypeShape::U8, "256").is_err());
468        assert!(decode(&rt, &TypeShape::U8, "-1").is_err());
469        assert!(matches!(
470            decode(&rt, &TypeShape::Unit, "null").unwrap(),
471            Value::Unit
472        ));
473        assert!(decode(&rt, &TypeShape::Unit, "0").is_err());
474    }
475
476    #[test]
477    fn float_forms() {
478        let rt = Runtime::new();
479        assert!(matches!(
480            decode(&rt, &TypeShape::F64, "1.5").unwrap(),
481            Value::F64(x) if x == 1.5
482        ));
483        assert!(matches!(
484            decode(&rt, &TypeShape::F64, "\"NaN\"").unwrap(),
485            Value::F64(x) if x.is_nan()
486        ));
487        assert!(matches!(
488            decode(&rt, &TypeShape::F64, "\"-Inf\"").unwrap(),
489            Value::F64(x) if x == f64::NEG_INFINITY
490        ));
491        assert!(decode(&rt, &TypeShape::F64, "\"nan\"").is_err());
492    }
493
494    #[test]
495    fn bigint_hybrid() {
496        let rt = Runtime::new();
497        let big = decode(&rt, &TypeShape::BigInt, "\"9007199254740992\"").unwrap();
498        let reencoded = encode(&rt, &big).unwrap();
499        assert_eq!(reencoded, "\"9007199254740992\"");
500        let small = decode(&rt, &TypeShape::BigInt, "12").unwrap();
501        assert_eq!(encode(&rt, &small).unwrap(), "12");
502        // An exact integer beyond 2^53 is accepted as a number too.
503        assert!(decode(&rt, &TypeShape::BigInt, "9007199254740993").is_ok());
504        assert!(decode(&rt, &TypeShape::BigInt, "1.5").is_err());
505        assert!(decode(&rt, &TypeShape::BigInt, "\"twelve\"").is_err());
506    }
507
508    #[test]
509    fn bytes_reject_non_canonical_base64() {
510        let rt = Runtime::new();
511        assert!(matches!(
512            decode(&rt, &TypeShape::Bytes, "\"aGVsbG8=\"").unwrap(),
513            Value::Bytes(b) if &*b == b"hello"
514        ));
515        assert!(decode(&rt, &TypeShape::Bytes, "\"aGVsbG8\"").is_err());
516        assert!(decode(&rt, &TypeShape::Bytes, "\"a GVsbG8=\"").is_err());
517    }
518
519    #[test]
520    fn record_strictness_and_ignored_refill() {
521        let (rt, id) = doc_runtime();
522        let shape = TypeShape::Named(id);
523        let value = decode(&rt, &shape, "{\"text\":\"hello\",\"id\":7}").unwrap();
524        // The ignored field was refilled from its default thunk.
525        let Value::Record(record) = &value else {
526            panic!()
527        };
528        assert!(matches!(record.fields[1], Value::U64(5)));
529        // Round trip: re-encoding omits the ignored field again.
530        assert_eq!(
531            encode(&rt, &value).unwrap(),
532            "{\"text\":\"hello\",\"id\":7}"
533        );
534        // Field order in input JSON does not matter (objects are unordered) …
535        let reordered = decode(&rt, &shape, "{\"id\":7,\"text\":\"hello\"}").unwrap();
536        assert!(ops::eq(&rt, &value, &reordered).unwrap());
537        // … but strictness does.
538        assert!(decode(&rt, &shape, "{\"text\":\"hello\"}").is_err());
539        assert!(decode(&rt, &shape, "{\"text\":\"hello\",\"id\":7,\"x\":0}").is_err());
540        assert!(decode(
541            &rt,
542            &shape,
543            "{\"text\":\"hello\",\"id\":7,\"cached_len\":5}"
544        )
545        .is_err());
546        assert!(decode(&rt, &shape, "{\"text\":\"hello\",\"id\":7,\"id\":8}").is_err());
547    }
548
549    #[test]
550    fn sum_and_bool() {
551        let mut rt = Runtime::new();
552        let id = rt
553            .registry
554            .register(TypeDesc {
555                name: "Shape".to_string(),
556                strategy: Strategy::Structural,
557                body: TypeBody::Sum(vec![
558                    VariantDesc {
559                        name: "Point".to_string(),
560                        payload: None,
561                    },
562                    VariantDesc {
563                        name: "Circle".to_string(),
564                        payload: Some(TypeShape::F64),
565                    },
566                ]),
567            })
568            .unwrap();
569        let shape = TypeShape::Named(id);
570        let circle = decode(&rt, &shape, "{\"tag\":\"Circle\",\"value\":2.5}").unwrap();
571        assert_eq!(
572            encode(&rt, &circle).unwrap(),
573            "{\"tag\":\"Circle\",\"value\":2.5}"
574        );
575        assert!(decode(&rt, &shape, "{\"tag\":\"Point\"}").is_ok());
576        assert!(decode(&rt, &shape, "{\"tag\":\"Square\"}").is_err());
577        assert!(decode(&rt, &shape, "{\"tag\":\"Point\",\"value\":1}").is_err());
578        assert!(decode(&rt, &shape, "{\"tag\":\"Circle\"}").is_err());
579        assert!(decode(&rt, &shape, "{\"tag\":\"Circle\",\"value\":2.5,\"x\":0}").is_err());
580
581        let bool_shape = TypeShape::Named(rt.registry.bool_id());
582        let t = decode(&rt, &bool_shape, "true").unwrap();
583        assert_eq!(encode(&rt, &t).unwrap(), "true");
584        assert!(decode(&rt, &bool_shape, "{\"tag\":\"True\"}").is_err());
585    }
586
587    #[test]
588    fn map_decode_sorts_structurally_and_rejects_duplicates() {
589        let rt = Runtime::new();
590        let shape = TypeShape::Map(Box::new(TypeShape::I64), Box::new(TypeShape::Utf8));
591        let value = decode(
592            &rt,
593            &shape,
594            "[{\"key\":2,\"value\":\"b\"},{\"key\":1,\"value\":\"a\"}]",
595        )
596        .unwrap();
597        assert_eq!(
598            encode(&rt, &value).unwrap(),
599            "[{\"key\":1,\"value\":\"a\"},{\"key\":2,\"value\":\"b\"}]"
600        );
601        assert!(decode(
602            &rt,
603            &shape,
604            "[{\"key\":1,\"value\":\"a\"},{\"key\":1,\"value\":\"b\"}]"
605        )
606        .is_err());
607        assert!(decode(&rt, &shape, "[{\"key\":1}]").is_err());
608        assert!(decode(&rt, &shape, "[{\"key\":1,\"value\":\"a\",\"z\":0}]").is_err());
609    }
610
611    #[test]
612    fn opaque_and_closures_do_not_decode() {
613        let mut rt = Runtime::new();
614        let id = rt
615            .registry
616            .register(TypeDesc {
617                name: "Handle".to_string(),
618                strategy: Strategy::Opaque,
619                body: TypeBody::Opaque,
620            })
621            .unwrap();
622        assert!(decode(&rt, &TypeShape::Named(id), "\"<handle>\"").is_err());
623        assert!(decode(&rt, &TypeShape::Closure, "null").is_err());
624    }
625
626    #[test]
627    fn duplicate_object_keys_rejected_at_parse() {
628        let (rt, id) = doc_runtime();
629        let result = decode(
630            &rt,
631            &TypeShape::Named(id),
632            "{\"text\":\"a\",\"text\":\"b\",\"id\":1}",
633        );
634        assert!(result.is_err());
635    }
636}