Skip to main content

reddb_types/utils/
json.rs

1//! Minimal JSON parser and serializer with zero dependencies.
2//! Implements a subset of JSON sufficient for MCP message handling.
3
4use std::fmt;
5
6/// Simplified JSON value representation.
7#[derive(Clone, Debug, PartialEq)]
8pub enum JsonValue {
9    Null,
10    Bool(bool),
11    Integer(i64),
12    Number(f64),
13    Decimal(String),
14    String(String),
15    Array(Vec<JsonValue>),
16    Object(Vec<(String, JsonValue)>),
17}
18
19impl JsonValue {
20    /// Returns the value as string reference if it is a string.
21    pub fn as_str(&self) -> Option<&str> {
22        match self {
23            JsonValue::String(s) => Some(s.as_str()),
24            _ => None,
25        }
26    }
27
28    /// Returns the value as f64 if it is a number.
29    pub fn as_f64(&self) -> Option<f64> {
30        match self {
31            JsonValue::Number(n) => Some(*n),
32            JsonValue::Integer(n) => Some(*n as f64),
33            JsonValue::Decimal(n) => n.parse::<f64>().ok(),
34            _ => None,
35        }
36    }
37
38    /// Returns the value as i64 if it is an exact integer.
39    pub fn as_i64(&self) -> Option<i64> {
40        match self {
41            JsonValue::Integer(n) => Some(*n),
42            _ => None,
43        }
44    }
45
46    /// Returns the value as boolean.
47    pub fn as_bool(&self) -> Option<bool> {
48        match self {
49            JsonValue::Bool(b) => Some(*b),
50            _ => None,
51        }
52    }
53
54    /// Returns the value as array.
55    pub fn as_array(&self) -> Option<&[JsonValue]> {
56        match self {
57            JsonValue::Array(items) => Some(items.as_slice()),
58            _ => None,
59        }
60    }
61
62    /// Returns the value as mutable array.
63    pub fn as_array_mut(&mut self) -> Option<&mut Vec<JsonValue>> {
64        match self {
65            JsonValue::Array(items) => Some(items),
66            _ => None,
67        }
68    }
69
70    /// Returns the object entries if the value is an object.
71    pub fn as_object(&self) -> Option<&[(String, JsonValue)]> {
72        match self {
73            JsonValue::Object(entries) => Some(entries.as_slice()),
74            _ => None,
75        }
76    }
77
78    /// Returns a mutable reference to the object entries.
79    pub fn as_object_mut(&mut self) -> Option<&mut Vec<(String, JsonValue)>> {
80        match self {
81            JsonValue::Object(entries) => Some(entries),
82            _ => None,
83        }
84    }
85
86    /// Retrieves field value from object by key.
87    pub fn get(&self, key: &str) -> Option<&JsonValue> {
88        if let JsonValue::Object(entries) = self {
89            for (k, v) in entries {
90                if k == key {
91                    return Some(v);
92                }
93            }
94        }
95        None
96    }
97
98    /// Retrieves mutable field value from object by key.
99    pub fn get_mut(&mut self, key: &str) -> Option<&mut JsonValue> {
100        if let JsonValue::Object(entries) = self {
101            for (k, v) in entries.iter_mut() {
102                if k == key {
103                    return Some(v);
104                }
105            }
106        }
107        None
108    }
109
110    /// Convenience constructor for JSON objects.
111    pub fn object(entries: Vec<(String, JsonValue)>) -> JsonValue {
112        JsonValue::Object(entries)
113    }
114
115    /// Convenience constructor for JSON arrays.
116    pub fn array(items: Vec<JsonValue>) -> JsonValue {
117        JsonValue::Array(items)
118    }
119
120    /// Serializes the value into a compact JSON string.
121    ///
122    /// **Deprecation note (ADR 0010 / issue #177):** the canonical
123    /// JSON encoder for serialization-boundary-sensitive paths
124    /// (audit log, HelloAck, PayloadReply, anything reaching a
125    /// downstream parser) is `crate::serde_json::Value::escape_string`
126    /// using `to_string_compact`. This local encoder is correct after
127    /// the F-01 hotfix (#181) but is not the canonical owner; new
128    /// audit / wire emission code should not call it. Existing MCP
129    /// JSON-RPC callers may keep using it pending a follow-up
130    /// retirement slice.
131    #[deprecated(
132        note = "Use crate::serde_json::Value::to_string_compact for boundary emission; see ADR 0010 / issue #177"
133    )]
134    pub fn to_json_string(&self) -> String {
135        let mut out = String::new();
136        self.write_json(&mut out);
137        out
138    }
139
140    fn write_json(&self, out: &mut String) {
141        match self {
142            JsonValue::Null => out.push_str("null"),
143            JsonValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
144            JsonValue::Integer(n) => out.push_str(&format!("{n}")),
145            JsonValue::Number(n) => {
146                if n.fract() == 0.0 {
147                    out.push_str(&format!("{}", *n as i64));
148                } else {
149                    out.push_str(&format!("{}", n));
150                }
151            }
152            JsonValue::Decimal(n) => out.push_str(n),
153            JsonValue::String(s) => {
154                out.push('"');
155                for ch in s.chars() {
156                    match ch {
157                        '"' => out.push_str("\\\""),
158                        '\\' => out.push_str("\\\\"),
159                        '\n' => out.push_str("\\n"),
160                        '\r' => out.push_str("\\r"),
161                        '\t' => out.push_str("\\t"),
162                        c if c.is_control() => {
163                            out.push_str(&format!("\\u{:04x}", c as u32));
164                        }
165                        c => out.push(c),
166                    }
167                }
168                out.push('"');
169            }
170            JsonValue::Array(items) => {
171                out.push('[');
172                for (idx, item) in items.iter().enumerate() {
173                    if idx > 0 {
174                        out.push(',');
175                    }
176                    item.write_json(out);
177                }
178                out.push(']');
179            }
180            JsonValue::Object(entries) => {
181                out.push('{');
182                for (idx, (key, value)) in entries.iter().enumerate() {
183                    if idx > 0 {
184                        out.push(',');
185                    }
186                    JsonValue::String(key.clone()).write_json(out);
187                    out.push(':');
188                    value.write_json(out);
189                }
190                out.push('}');
191            }
192        }
193    }
194}
195
196impl From<&str> for JsonValue {
197    fn from(value: &str) -> JsonValue {
198        JsonValue::String(value.to_string())
199    }
200}
201
202impl From<String> for JsonValue {
203    fn from(value: String) -> JsonValue {
204        JsonValue::String(value)
205    }
206}
207
208impl From<bool> for JsonValue {
209    fn from(value: bool) -> JsonValue {
210        JsonValue::Bool(value)
211    }
212}
213
214impl From<f64> for JsonValue {
215    fn from(value: f64) -> JsonValue {
216        JsonValue::Number(value)
217    }
218}
219
220impl From<i64> for JsonValue {
221    fn from(value: i64) -> JsonValue {
222        JsonValue::Integer(value)
223    }
224}
225
226impl From<usize> for JsonValue {
227    fn from(value: usize) -> JsonValue {
228        i64::try_from(value)
229            .map(JsonValue::Integer)
230            .unwrap_or_else(|_| JsonValue::Number(value as f64))
231    }
232}
233
234impl fmt::Display for JsonValue {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        // Internal route โ€” Display is the legacy entry point and
237        // routes through the (now deprecated for boundary use)
238        // `to_json_string`. Silence the warning here so the lint
239        // surfaces only at external call sites.
240        #[allow(deprecated)]
241        let s = self.to_json_string();
242        write!(f, "{s}")
243    }
244}
245
246/// Parser for JSON strings into [`JsonValue`].
247pub struct JsonParser<'a> {
248    input: &'a [u8],
249    pos: usize,
250}
251
252impl<'a> JsonParser<'a> {
253    /// Creates a new parser from input slice.
254    pub fn new(input: &'a str) -> Self {
255        Self {
256            input: input.as_bytes(),
257            pos: 0,
258        }
259    }
260
261    /// Parses a JSON value from the current position.
262    pub fn parse_value(&mut self) -> Result<JsonValue, String> {
263        self.skip_whitespace();
264        if self.eof() {
265            return Err("unexpected end of input".to_string());
266        }
267        let ch = self.current_char();
268        match ch {
269            b'n' => self.parse_null(),
270            b't' | b'f' => self.parse_bool(),
271            b'-' | b'0'..=b'9' => self.parse_number(),
272            b'"' => self.parse_string().map(JsonValue::String),
273            b'[' => self.parse_array(),
274            b'{' => self.parse_object(),
275            _ => Err(format!("unexpected character '{}'", ch as char)),
276        }
277    }
278
279    fn parse_null(&mut self) -> Result<JsonValue, String> {
280        self.expect_bytes(b"null")?;
281        Ok(JsonValue::Null)
282    }
283
284    fn parse_bool(&mut self) -> Result<JsonValue, String> {
285        if self.matches_bytes(b"true") {
286            self.pos += 4;
287            Ok(JsonValue::Bool(true))
288        } else if self.matches_bytes(b"false") {
289            self.pos += 5;
290            Ok(JsonValue::Bool(false))
291        } else {
292            Err("invalid boolean literal".to_string())
293        }
294    }
295
296    fn parse_number(&mut self) -> Result<JsonValue, String> {
297        let start = self.pos;
298        if self.current_char() == b'-' {
299            self.pos += 1;
300        }
301        if self.eof() {
302            return Err("invalid number literal".to_string());
303        }
304
305        match self.current_char() {
306            b'0' => {
307                self.pos += 1;
308            }
309            b'1'..=b'9' => {
310                self.pos += 1;
311                while !self.eof() && self.current_char().is_ascii_digit() {
312                    self.pos += 1;
313                }
314            }
315            _ => return Err("invalid number literal".to_string()),
316        }
317
318        let mut is_float = false;
319
320        if !self.eof() && self.current_char() == b'.' {
321            is_float = true;
322            self.pos += 1;
323            if self.eof() || !self.current_char().is_ascii_digit() {
324                return Err("invalid number literal".to_string());
325            }
326            while !self.eof() && self.current_char().is_ascii_digit() {
327                self.pos += 1;
328            }
329        }
330
331        if !self.eof() && (self.current_char() == b'e' || self.current_char() == b'E') {
332            is_float = true;
333            self.pos += 1;
334            if !self.eof() && (self.current_char() == b'+' || self.current_char() == b'-') {
335                self.pos += 1;
336            }
337            if self.eof() || !self.current_char().is_ascii_digit() {
338                return Err("invalid number literal".to_string());
339            }
340            while !self.eof() && self.current_char().is_ascii_digit() {
341                self.pos += 1;
342            }
343        }
344
345        let slice = &self.input[start..self.pos];
346        let s = std::str::from_utf8(slice).map_err(|_| "invalid UTF-8 in number".to_string())?;
347        if !is_float {
348            if let Ok(value) = s.parse::<i64>() {
349                return Ok(JsonValue::Integer(value));
350            }
351            return Ok(JsonValue::Decimal(s.to_string()));
352        }
353        if should_preserve_decimal_text(s) {
354            return Ok(JsonValue::Decimal(s.to_string()));
355        }
356        let value = s
357            .parse::<f64>()
358            .map_err(|_| "failed to parse number".to_string())?;
359        if !value.is_finite() {
360            return Ok(JsonValue::Decimal(s.to_string()));
361        }
362        Ok(JsonValue::Number(value))
363    }
364
365    fn parse_string(&mut self) -> Result<String, String> {
366        self.expect_char(b'"')?;
367        let mut result = String::new();
368        while !self.eof() {
369            let ch = self.current_char();
370            match ch {
371                b'"' => {
372                    self.pos += 1;
373                    return Ok(result);
374                }
375                b'\\' => {
376                    self.pos += 1;
377                    if self.eof() {
378                        return Err("unexpected end of input in escape".to_string());
379                    }
380                    let esc = self.current_char();
381                    self.pos += 1;
382                    match esc {
383                        b'"' => result.push('"'),
384                        b'\\' => result.push('\\'),
385                        b'/' => result.push('/'),
386                        b'b' => result.push('\x08'),
387                        b'f' => result.push('\x0c'),
388                        b'n' => result.push('\n'),
389                        b'r' => result.push('\r'),
390                        b't' => result.push('\t'),
391                        b'u' => {
392                            // RFC 8259 ยง7: `\uXXXX` covers code points in
393                            // the BMP. Code points above U+FFFF (e.g. emoji,
394                            // U+1F4A9 PILE OF POO) are represented in JSON
395                            // as a UTF-16 surrogate pair `๐Ÿ’ฉ` and
396                            // MUST be decoded as one Unicode scalar.
397                            let code = self.parse_unicode_escape()?;
398                            if (0xD800..=0xDBFF).contains(&code) {
399                                // High surrogate โ€” must be followed by a
400                                // `\uXXXX` low surrogate.
401                                if self.pos + 2 > self.input.len()
402                                    || self.input[self.pos] != b'\\'
403                                    || self.input[self.pos + 1] != b'u'
404                                {
405                                    return Err(
406                                        "expected low surrogate after high surrogate".to_string()
407                                    );
408                                }
409                                self.pos += 2;
410                                let low = self.parse_unicode_escape()?;
411                                if !(0xDC00..=0xDFFF).contains(&low) {
412                                    return Err("invalid low surrogate".to_string());
413                                }
414                                let scalar = 0x10000 + (((code - 0xD800) << 10) | (low - 0xDC00));
415                                if let Some(chr) = char::from_u32(scalar) {
416                                    result.push(chr);
417                                } else {
418                                    return Err("invalid unicode escape".to_string());
419                                }
420                            } else if (0xDC00..=0xDFFF).contains(&code) {
421                                return Err(
422                                    "unexpected low surrogate without preceding high surrogate"
423                                        .to_string(),
424                                );
425                            } else if let Some(chr) = char::from_u32(code) {
426                                result.push(chr);
427                            } else {
428                                return Err("invalid unicode escape".to_string());
429                            }
430                        }
431                        _ => return Err("invalid escape sequence".to_string()),
432                    }
433                }
434                _ if ch < 0x80 => {
435                    // ASCII fast path: byte == codepoint.
436                    self.pos += 1;
437                    result.push(ch as char);
438                }
439                _ => {
440                    // Multi-byte UTF-8 sequence. The parser's input came
441                    // from `&str` (validated UTF-8 at JsonParser::new), so
442                    // a complete codepoint starts here. Decoding byte-by-
443                    // byte via `ch as char` would map each continuation
444                    // byte to a Latin-1 codepoint, producing mojibake (the
445                    // root cause of issue #191 โ€” `รฉ` โ†’ `รƒยฉ`, `๐Ÿฆ€` โ†’ four
446                    // garbled chars). Instead, lift a full codepoint out
447                    // of the underlying UTF-8 stream.
448                    let next = std::str::from_utf8(&self.input[self.pos..])
449                        .map_err(|_| "invalid utf-8 in string body".to_string())?
450                        .chars()
451                        .next()
452                        .ok_or_else(|| "unterminated string literal".to_string())?;
453                    self.pos += next.len_utf8();
454                    result.push(next);
455                }
456            }
457        }
458        Err("unterminated string literal".to_string())
459    }
460
461    fn parse_unicode_escape(&mut self) -> Result<u32, String> {
462        if self.pos + 4 > self.input.len() {
463            return Err("invalid unicode escape".to_string());
464        }
465        let mut value = 0u32;
466        for _ in 0..4 {
467            let ch = self.current_char();
468            self.pos += 1;
469            value <<= 4;
470            value |= match ch {
471                b'0'..=b'9' => (ch - b'0') as u32,
472                b'a'..=b'f' => (ch - b'a' + 10) as u32,
473                b'A'..=b'F' => (ch - b'A' + 10) as u32,
474                _ => return Err("invalid unicode escape".to_string()),
475            };
476        }
477        Ok(value)
478    }
479
480    fn parse_array(&mut self) -> Result<JsonValue, String> {
481        self.expect_char(b'[')?;
482        let mut items = Vec::new();
483        self.skip_whitespace();
484        if self.peek_char() == Some(b']') {
485            self.pos += 1;
486            return Ok(JsonValue::Array(items));
487        }
488        loop {
489            let value = self.parse_value()?;
490            items.push(value);
491            self.skip_whitespace();
492            match self.peek_char() {
493                Some(b',') => {
494                    self.pos += 1;
495                }
496                Some(b']') => {
497                    self.pos += 1;
498                    break;
499                }
500                _ => return Err("expected ',' or ']' in array".to_string()),
501            }
502        }
503        Ok(JsonValue::Array(items))
504    }
505
506    fn parse_object(&mut self) -> Result<JsonValue, String> {
507        self.expect_char(b'{')?;
508        let mut entries = Vec::new();
509        self.skip_whitespace();
510        if self.peek_char() == Some(b'}') {
511            self.pos += 1;
512            return Ok(JsonValue::Object(entries));
513        }
514        loop {
515            self.skip_whitespace();
516            let key = self.parse_string()?;
517            self.skip_whitespace();
518            self.expect_char(b':')?;
519            let value = self.parse_value()?;
520            entries.push((key, value));
521            self.skip_whitespace();
522            match self.peek_char() {
523                Some(b',') => {
524                    self.pos += 1;
525                }
526                Some(b'}') => {
527                    self.pos += 1;
528                    break;
529                }
530                _ => return Err("expected ',' or '}' in object".to_string()),
531            }
532        }
533        Ok(JsonValue::Object(entries))
534    }
535
536    fn skip_whitespace(&mut self) {
537        while !self.eof() {
538            match self.current_char() {
539                b' ' | b'\n' | b'\r' | b'\t' => self.pos += 1,
540                _ => break,
541            }
542        }
543    }
544
545    fn expect_bytes(&mut self, expected: &[u8]) -> Result<(), String> {
546        if self.remaining().starts_with(expected) {
547            self.pos += expected.len();
548            Ok(())
549        } else {
550            Err("unexpected token".to_string())
551        }
552    }
553
554    fn matches_bytes(&self, expected: &[u8]) -> bool {
555        self.remaining().starts_with(expected)
556    }
557
558    fn expect_char(&mut self, expected: u8) -> Result<(), String> {
559        if self.peek_char() == Some(expected) {
560            self.pos += 1;
561            Ok(())
562        } else {
563            Err(format!("expected '{}'", expected as char))
564        }
565    }
566
567    fn peek_char(&self) -> Option<u8> {
568        if self.pos >= self.input.len() {
569            None
570        } else {
571            Some(self.input[self.pos])
572        }
573    }
574
575    fn current_char(&self) -> u8 {
576        self.input[self.pos]
577    }
578
579    fn remaining(&self) -> &[u8] {
580        &self.input[self.pos..]
581    }
582
583    fn eof(&self) -> bool {
584        self.pos >= self.input.len()
585    }
586}
587
588fn should_preserve_decimal_text(input: &str) -> bool {
589    let significant_digits = input.bytes().filter(u8::is_ascii_digit).count();
590    significant_digits > 15
591}
592
593/// Parses the provided JSON string into a [`JsonValue`].
594pub fn parse_json(input: &str) -> Result<JsonValue, String> {
595    let mut parser = JsonParser::new(input);
596    let value = parser.parse_value()?;
597    parser.skip_whitespace();
598    if parser.eof() {
599        Ok(value)
600    } else {
601        Err("unexpected trailing data".to_string())
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use proptest::prelude::*;
609
610    #[test]
611    fn parse_simple_object() {
612        let json = r#"{"name":"reddb","active":true,"count":3}"#;
613        let value = parse_json(json).unwrap();
614        let obj = value.as_object().unwrap();
615        assert_eq!(obj.len(), 3);
616    }
617
618    #[test]
619    fn parse_nested_array() {
620        let json = r#"{"items":[1,2,{"flag":false}]}"#;
621        let value = parse_json(json).unwrap();
622        assert!(value.get("items").unwrap().as_array().is_some());
623    }
624
625    #[test]
626    fn stringify_roundtrip() {
627        let json = r#"{"message":"hello","value":42}"#;
628        let value = parse_json(json).unwrap();
629        #[allow(deprecated)]
630        let output = value.to_json_string();
631        assert!(output.contains("hello"));
632    }
633
634    #[test]
635    fn constructors_accessors_mutators_and_display_cover_all_shapes() {
636        let mut value = JsonValue::object(vec![
637            ("name".to_string(), JsonValue::from("reddb")),
638            ("active".to_string(), JsonValue::from(true)),
639            ("count".to_string(), JsonValue::from(3usize)),
640            ("ratio".to_string(), JsonValue::from(1.5f64)),
641            (
642                "items".to_string(),
643                JsonValue::array(vec![JsonValue::Null, JsonValue::from(2i64)]),
644            ),
645        ]);
646
647        assert_eq!(value.get("name").and_then(JsonValue::as_str), Some("reddb"));
648        assert_eq!(value.get("active").and_then(JsonValue::as_bool), Some(true));
649        assert_eq!(value.get("count").and_then(JsonValue::as_f64), Some(3.0));
650        assert_eq!(
651            value
652                .get("items")
653                .and_then(JsonValue::as_array)
654                .map(<[_]>::len),
655            Some(2)
656        );
657        assert_eq!(value.as_object().map(<[_]>::len), Some(5));
658        assert!(JsonValue::Null.as_str().is_none());
659        assert!(JsonValue::Null.as_bool().is_none());
660        assert!(JsonValue::Null.as_array().is_none());
661        assert!(JsonValue::Null.as_object().is_none());
662
663        value
664            .get_mut("count")
665            .map(|slot| *slot = JsonValue::from(4usize))
666            .unwrap();
667        assert_eq!(value.get("count").and_then(JsonValue::as_f64), Some(4.0));
668
669        value
670            .as_object_mut()
671            .unwrap()
672            .push(("extra".to_string(), JsonValue::from("ok".to_string())));
673        let mut array = JsonValue::array(vec![JsonValue::from(1usize)]);
674        array.as_array_mut().unwrap().push(JsonValue::from(2usize));
675        assert_eq!(array.as_array().unwrap().len(), 2);
676
677        #[allow(deprecated)]
678        let encoded =
679            JsonValue::String("quote\" slash\\ tab\t ctrl\x01".to_string()).to_json_string();
680        assert!(encoded.contains("\\\""));
681        assert!(encoded.contains("\\\\"));
682        assert!(encoded.contains("\\t"));
683        assert!(encoded.contains("\\u0001"));
684        assert_eq!(format!("{}", JsonValue::Bool(false)), "false");
685    }
686
687    #[test]
688    fn parser_accepts_scalars_escapes_empty_containers_and_trailing_space() {
689        assert_eq!(parse_json(" null \n").unwrap(), JsonValue::Null);
690        assert_eq!(parse_json("true").unwrap(), JsonValue::Bool(true));
691        assert_eq!(parse_json("false").unwrap(), JsonValue::Bool(false));
692        assert_eq!(parse_json("-12.5e+2").unwrap(), JsonValue::Number(-1250.0));
693        assert_eq!(
694            parse_json("3.14159265358979323846").unwrap(),
695            JsonValue::Decimal("3.14159265358979323846".to_string())
696        );
697        assert_eq!(
698            parse_json("18446744073709551616").unwrap(),
699            JsonValue::Decimal("18446744073709551616".to_string())
700        );
701        assert_eq!(parse_json("[]").unwrap(), JsonValue::Array(Vec::new()));
702        assert_eq!(parse_json("{}").unwrap(), JsonValue::Object(Vec::new()));
703
704        let escaped = parse_json(r#""\"\\\/\b\f\n\r\t\u00e9\uD83D\uDCA9""#).unwrap();
705        let text = escaped.as_str().unwrap();
706        assert!(text.contains('"'));
707        assert!(text.contains('\\'));
708        assert!(text.contains('/'));
709        assert!(text.contains('\x08'));
710        assert!(text.contains('\x0c'));
711        assert!(text.contains('\n'));
712        assert!(text.contains('\r'));
713        assert!(text.contains('\t'));
714        assert!(text.chars().any(|ch| ch as u32 == 0x00E9));
715        assert_eq!(text.chars().last(), char::from_u32(0x1F4A9));
716    }
717
718    #[test]
719    fn parser_rejects_invalid_tokens_numbers_strings_and_separators() {
720        let cases = [
721            "",
722            "truth",
723            "nul",
724            "-",
725            "01",
726            "1.",
727            "1e",
728            r#""unterminated"#,
729            r#""\x""#,
730            r#""\u12""#,
731            r#""\u12zz""#,
732            r#""\uD800""#,
733            r#""\uD800\u0000""#,
734            r#""\uDC00""#,
735            "[1 2]",
736            "[1,]",
737            r#"{"a" 1}"#,
738            r#"{"a":1 "b":2}"#,
739            "{} trailing",
740            "?",
741        ];
742
743        for case in cases {
744            assert!(parse_json(case).is_err(), "{case:?}");
745        }
746    }
747
748    proptest! {
749        #[test]
750        fn beyond_native_decimal_text_round_trips(
751            prefix in "[1-9][0-9]{18,36}",
752            suffix in "[0-9]{1,24}"
753        ) {
754            let input = format!("{prefix}.{suffix}");
755            let parsed = parse_json(&input).expect("generated decimal parses");
756            prop_assert_eq!(&parsed, &JsonValue::Decimal(input.clone()));
757            #[allow(deprecated)]
758            let output = parsed.to_json_string();
759            prop_assert_eq!(output, input);
760        }
761    }
762}