Skip to main content

oxml_json/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 oxml-json. All rights reserved.
3
4//! A small JSON value, parser and serialiser.
5//!
6//! This exists because two crates in the [oxml] suite speak JSON-RPC
7//! — `oxml-mcp` over the Model Context Protocol and `oxml-lsp` over
8//! the Language Server Protocol — and for a while each carried its own
9//! copy of this file. A defect fixed in one would not have reached the
10//! other, and nothing would have noticed.
11//!
12//! [oxml]: https://github.com/sebastienrousseau/oxml
13//!
14//! # Scope
15//!
16//! JSON-RPC messages are small and their shapes are fixed, so this
17//! implements what those protocols need rather than everything the
18//! grammar allows. It is not a general-purpose JSON library and does
19//! not try to be: if you want `serde`, use `serde`.
20//!
21//! What it does do is refuse malformed input rather than guess at it,
22//! and it is fuzzed on every pull request.
23//!
24//! # Numbers
25//!
26//! Numbers are `f64`, which is what JSON's grammar describes and what
27//! both protocols use for ids and codes. An integer beyond 2^53 does
28//! not survive, which no request id in either protocol reaches.
29//!
30//! ```
31//! use oxml_json::{parse, Json};
32//!
33//! let value = parse(r#"{"jsonrpc":"2.0","id":1}"#).expect("valid JSON");
34//! assert_eq!(value.get("jsonrpc").and_then(Json::as_str), Some("2.0"));
35//! ```
36
37#![forbid(unsafe_code)]
38#![deny(missing_docs)]
39
40use std::collections::BTreeMap;
41use std::fmt::Write as _;
42
43/// A JSON value.
44#[derive(Debug, Clone, PartialEq)]
45pub enum Json {
46    /// `null`
47    Null,
48    /// `true` or `false`
49    Bool(bool),
50    /// A number.
51    Number(f64),
52    /// A string.
53    String(String),
54    /// An array.
55    Array(Vec<Json>),
56    /// An object. Ordered so output is deterministic.
57    Object(BTreeMap<String, Json>),
58}
59
60impl Json {
61    /// Build an object from pairs.
62    #[must_use]
63    pub fn object(pairs: Vec<(&str, Self)>) -> Self {
64        Self::Object(
65            pairs.into_iter().map(|(k, v)| (k.to_owned(), v)).collect(),
66        )
67    }
68
69    /// A string value.
70    #[must_use]
71    pub fn str(s: impl Into<String>) -> Self {
72        Self::String(s.into())
73    }
74
75    /// Look up a key on an object.
76    #[must_use]
77    pub fn get(&self, key: &str) -> Option<&Self> {
78        match self {
79            Self::Object(m) => m.get(key),
80            _ => None,
81        }
82    }
83
84    /// The value as a string slice, if it is one.
85    #[must_use]
86    pub fn as_str(&self) -> Option<&str> {
87        match self {
88            Self::String(s) => Some(s),
89            _ => None,
90        }
91    }
92
93    /// Serialise to JSON text.
94    #[must_use]
95    pub fn to_json(&self) -> String {
96        let mut out = String::new();
97        self.write(&mut out);
98        out
99    }
100
101    fn write(&self, out: &mut String) {
102        match self {
103            Self::Null => out.push_str("null"),
104            Self::Bool(b) => {
105                out.push_str(if *b { "true" } else { "false" });
106            }
107            Self::Number(n) => {
108                // JSON has one number type; printing an integral
109                // value as `2` rather than `2.0` is what every other
110                // encoder does, and the bound keeps the cast exact.
111                #[allow(clippy::cast_possible_truncation)]
112                if n.fract() == 0.0 && n.abs() < 1e15 {
113                    let _ = write!(out, "{}", *n as i64);
114                } else {
115                    let _ = write!(out, "{n}");
116                }
117            }
118            Self::String(s) => write_string(s, out),
119            Self::Array(items) => {
120                out.push('[');
121                for (i, item) in items.iter().enumerate() {
122                    if i > 0 {
123                        out.push(',');
124                    }
125                    item.write(out);
126                }
127                out.push(']');
128            }
129            Self::Object(map) => {
130                out.push('{');
131                for (i, (k, v)) in map.iter().enumerate() {
132                    if i > 0 {
133                        out.push(',');
134                    }
135                    write_string(k, out);
136                    out.push(':');
137                    v.write(out);
138                }
139                out.push('}');
140            }
141        }
142    }
143}
144
145fn write_string(s: &str, out: &mut String) {
146    out.push('"');
147    for c in s.chars() {
148        match c {
149            '"' => out.push_str("\\\""),
150            '\\' => out.push_str("\\\\"),
151            '\n' => out.push_str("\\n"),
152            '\r' => out.push_str("\\r"),
153            '\t' => out.push_str("\\t"),
154            // Control characters must be escaped or the output is not
155            // valid JSON, and XML documents do contain them.
156            c if (c as u32) < 0x20 => {
157                let _ = write!(out, "\\u{:04x}", c as u32);
158            }
159            c => out.push(c),
160        }
161    }
162    out.push('"');
163}
164
165/// Parse JSON text.
166///
167/// # Errors
168///
169/// Returns a description if the input is not valid JSON.
170pub fn parse(input: &str) -> Result<Json, String> {
171    let chars: Vec<char> = input.chars().collect();
172    let mut p = Parser {
173        chars: &chars,
174        pos: 0,
175    };
176    p.ws();
177    let v = p.value()?;
178    p.ws();
179    if p.pos < chars.len() {
180        return Err("trailing input after JSON value".to_owned());
181    }
182    Ok(v)
183}
184
185struct Parser<'a> {
186    chars: &'a [char],
187    pos: usize,
188}
189
190impl Parser<'_> {
191    fn ws(&mut self) {
192        while matches!(self.chars.get(self.pos), Some(' ' | '\t' | '\n' | '\r'))
193        {
194            self.pos += 1;
195        }
196    }
197
198    fn value(&mut self) -> Result<Json, String> {
199        self.ws();
200        match self.chars.get(self.pos) {
201            Some('{') => self.object(),
202            Some('[') => self.array(),
203            Some('"') => Ok(Json::String(self.string()?)),
204            Some('t') => self.literal("true", Json::Bool(true)),
205            Some('f') => self.literal("false", Json::Bool(false)),
206            Some('n') => self.literal("null", Json::Null),
207            Some(_) => self.number(),
208            None => Err("unexpected end of JSON".to_owned()),
209        }
210    }
211
212    fn literal(&mut self, word: &str, v: Json) -> Result<Json, String> {
213        if self.chars[self.pos..]
214            .starts_with(word.chars().collect::<Vec<_>>().as_slice())
215        {
216            self.pos += word.len();
217            Ok(v)
218        } else {
219            Err(format!("expected `{word}`"))
220        }
221    }
222
223    fn object(&mut self) -> Result<Json, String> {
224        self.pos += 1;
225        let mut map = BTreeMap::new();
226        self.ws();
227        if self.chars.get(self.pos) == Some(&'}') {
228            self.pos += 1;
229            return Ok(Json::Object(map));
230        }
231        loop {
232            self.ws();
233            let key = self.string()?;
234            self.ws();
235            if self.chars.get(self.pos) != Some(&':') {
236                return Err("expected `:` in object".to_owned());
237            }
238            self.pos += 1;
239            let value = self.value()?;
240            let _ = map.insert(key, value);
241            self.ws();
242            match self.chars.get(self.pos) {
243                Some(',') => self.pos += 1,
244                Some('}') => {
245                    self.pos += 1;
246                    return Ok(Json::Object(map));
247                }
248                _ => return Err("expected `,` or `}`".to_owned()),
249            }
250        }
251    }
252
253    fn array(&mut self) -> Result<Json, String> {
254        self.pos += 1;
255        let mut items = Vec::new();
256        self.ws();
257        if self.chars.get(self.pos) == Some(&']') {
258            self.pos += 1;
259            return Ok(Json::Array(items));
260        }
261        loop {
262            items.push(self.value()?);
263            self.ws();
264            match self.chars.get(self.pos) {
265                Some(',') => self.pos += 1,
266                Some(']') => {
267                    self.pos += 1;
268                    return Ok(Json::Array(items));
269                }
270                _ => return Err("expected `,` or `]`".to_owned()),
271            }
272        }
273    }
274
275    fn string(&mut self) -> Result<String, String> {
276        if self.chars.get(self.pos) != Some(&'"') {
277            return Err("expected a string".to_owned());
278        }
279        self.pos += 1;
280        let mut out = String::new();
281        loop {
282            match self.chars.get(self.pos) {
283                None => return Err("unterminated string".to_owned()),
284                Some('"') => {
285                    self.pos += 1;
286                    return Ok(out);
287                }
288                Some('\\') => {
289                    self.pos += 1;
290                    let esc =
291                        self.chars.get(self.pos).ok_or("trailing backslash")?;
292                    self.pos += 1;
293                    match esc {
294                        'n' => out.push('\n'),
295                        't' => out.push('\t'),
296                        'r' => out.push('\r'),
297                        'b' => out.push('\u{8}'),
298                        'f' => out.push('\u{c}'),
299                        'u' => {
300                            let cp = self.hex4()?;
301                            // A non-BMP character is escaped as a
302                            // surrogate *pair*; neither half is a
303                            // `char` on its own. Python's `json.dumps`
304                            // escapes non-ASCII by default, so a client
305                            // sending an emoji arrives this way.
306                            let cp = if (0xD800..0xDC00).contains(&cp) {
307                                if !matches!(
308                                    self.chars.get(self.pos..self.pos + 2),
309                                    Some(['\\', 'u'])
310                                ) {
311                                    return Err(
312                                        "lone high surrogate".to_owned()
313                                    );
314                                }
315                                self.pos += 2;
316                                let lo = self.hex4()?;
317                                if !(0xDC00..0xE000).contains(&lo) {
318                                    return Err(
319                                        "high surrogate not followed by a \
320                                         low surrogate"
321                                            .to_owned(),
322                                    );
323                                }
324                                0x1_0000 + ((cp - 0xD800) << 10) + (lo - 0xDC00)
325                            } else if (0xDC00..0xE000).contains(&cp) {
326                                return Err("lone low surrogate".to_owned());
327                            } else {
328                                cp
329                            };
330                            out.push(
331                                char::from_u32(cp).ok_or("bad code point")?,
332                            );
333                        }
334                        c => out.push(*c),
335                    }
336                }
337                Some(c) => {
338                    out.push(*c);
339                    self.pos += 1;
340                }
341            }
342        }
343    }
344
345    /// Read exactly four hex digits of a `\\u` escape.
346    fn hex4(&mut self) -> Result<u32, String> {
347        let hex: String = self
348            .chars
349            .get(self.pos..self.pos + 4)
350            .ok_or("truncated \\u escape")?
351            .iter()
352            .collect();
353        self.pos += 4;
354        u32::from_str_radix(&hex, 16).map_err(|_| "bad \\u escape".to_owned())
355    }
356
357    fn number(&mut self) -> Result<Json, String> {
358        let start = self.pos;
359        while matches!(
360            self.chars.get(self.pos),
361            Some(c) if c.is_ascii_digit()
362                || matches!(c, '-' | '+' | '.' | 'e' | 'E')
363        ) {
364            self.pos += 1;
365        }
366        let text: String = self.chars[start..self.pos].iter().collect();
367        let value: f64 = text
368            .parse()
369            .map_err(|_| format!("invalid number `{text}`"))?;
370        // `"1e999"` is grammatically valid JSON and parses to
371        // infinity, which JSON cannot express -- so serialising it
372        // again produced output that would not parse. Refusing it here
373        // is the honest answer for a parser that carries numbers as
374        // `f64`: the alternative, writing `null`, silently changes the
375        // value.
376        //
377        // Found by fuzzing, on `-3e805`, within ninety seconds of the
378        // round-trip assertion being added.
379        if !value.is_finite() {
380            return Err(format!("number `{text}` is out of range for f64"));
381        }
382        Ok(Json::Number(value))
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    fn round_trip(text: &str) -> String {
391        parse(text).expect("valid JSON").to_json()
392    }
393
394    #[test]
395    fn a_number_too_large_for_f64_is_refused() {
396        // Grammatically valid JSON that `f64` cannot hold. Parsing it
397        // to infinity and serialising that produced output which would
398        // not parse -- the round trip silently broke.
399        //
400        // Fuzzing found it on `-3e805`. Both directions are checked
401        // because the overflow is symmetric and only one sign was
402        // reported.
403        for text in ["1e999", "-3e805", "1e309"] {
404            let err = parse(text).expect_err("should be refused");
405            assert!(
406                err.contains("out of range"),
407                "{text} gave the wrong error: {err}"
408            );
409        }
410        // The largest finite double still parses, so the check bounds
411        // the value rather than the exponent's length.
412        assert!(parse("1.7976931348623157e308").is_ok());
413    }
414
415    #[test]
416    fn scalars_round_trip() {
417        assert_eq!(round_trip("null"), "null");
418        assert_eq!(round_trip("true"), "true");
419        assert_eq!(round_trip("false"), "false");
420        assert_eq!(round_trip("\"hi\""), "\"hi\"");
421    }
422
423    #[test]
424    fn integral_numbers_print_without_a_fraction() {
425        // MCP ids are compared by clients; `2.0` where `2` was sent is
426        // a different JSON value to a strict client.
427        assert_eq!(round_trip("2"), "2");
428        assert_eq!(round_trip("-17"), "-17");
429        assert_eq!(round_trip("0"), "0");
430        assert_eq!(round_trip("1.5"), "1.5");
431        assert_eq!(round_trip("1e3"), "1000");
432    }
433
434    #[test]
435    fn very_large_integral_numbers_do_not_wrap() {
436        // Beyond 1e15 the i64 cast would be lossy, so the float path
437        // must take over rather than silently truncating.
438        let out = Json::Number(1e300).to_json();
439        assert!(out.starts_with('1'), "{out}");
440        assert!(parse(&out).is_ok(), "{out} did not re-parse");
441    }
442
443    #[test]
444    fn objects_serialise_in_a_stable_order() {
445        let text = round_trip(r#"{"b":1,"a":2}"#);
446        assert_eq!(text, r#"{"a":2,"b":1}"#);
447    }
448
449    #[test]
450    fn nesting_round_trips() {
451        let src = r#"{"a":[1,[2,{"b":null}]],"c":{}}"#;
452        assert_eq!(round_trip(src), src);
453    }
454
455    #[test]
456    fn string_escapes_decode() {
457        let v = parse(r#""a\nb\tc\"d\\e\/f\r\b\f""#).expect("valid");
458        assert_eq!(v.as_str(), Some("a\nb\tc\"d\\e/f\r\u{8}\u{c}"));
459    }
460
461    #[test]
462    fn control_characters_are_escaped_on_output() {
463        // XML documents do contain these, and an unescaped control
464        // character makes the whole response unparseable to the client.
465        let out = Json::str("a\u{1}b\u{1f}").to_json();
466        assert_eq!(out, r#""a\u0001b\u001f""#);
467        assert_eq!(
468            parse(&out).expect("re-parses").as_str(),
469            Some("a\u{1}b\u{1f}")
470        );
471    }
472
473    #[test]
474    fn surrogate_pairs_decode_to_one_character() {
475        // Python's `json.dumps` escapes non-ASCII by default, so a
476        // client written against it sends emoji this way.
477        assert_eq!(
478            parse(r#""\ud83d\ude00""#).expect("valid").as_str(),
479            Some("\u{1f600}")
480        );
481        assert_eq!(
482            parse(r#""a\ud83d\ude00b""#).expect("valid").as_str(),
483            Some("a\u{1f600}b")
484        );
485    }
486
487    #[test]
488    fn bmp_escapes_still_decode() {
489        assert_eq!(
490            parse(r#""\u00e9\u4e2d""#).expect("valid").as_str(),
491            Some("é中")
492        );
493    }
494
495    #[test]
496    fn lone_surrogates_are_rejected() {
497        // Rather than panicking or emitting an invalid character.
498        assert!(parse(r#""\ud83d""#).is_err());
499        assert!(parse(r#""\ude00""#).is_err());
500        assert!(parse(r#""\ud83dx""#).is_err());
501        assert!(parse(r#""\ud83d\u0041""#).is_err());
502    }
503
504    #[test]
505    fn raw_utf8_passes_through_unescaped() {
506        let src = "\"héllo 😀 中\"";
507        assert_eq!(round_trip(src), src);
508    }
509
510    #[test]
511    fn malformed_input_is_an_error_not_a_panic() {
512        for bad in [
513            "",
514            "{",
515            "[",
516            "{\"a\"}",
517            "{\"a\":}",
518            "[1,",
519            "\"unterminated",
520            "tru",
521            "nul",
522            "\"\\",
523            "\"\\u00\"",
524            "\"\\uZZZZ\"",
525            "@",
526            "{\"a\":1}trailing",
527            "1 2",
528        ] {
529            assert!(parse(bad).is_err(), "`{bad}` should not parse");
530        }
531    }
532
533    #[test]
534    fn whitespace_around_values_is_ignored() {
535        assert_eq!(
536            round_trip(" \n\t{ \"a\" : [ 1 , 2 ] }\r\n"),
537            r#"{"a":[1,2]}"#
538        );
539    }
540
541    #[test]
542    fn get_and_as_str_are_type_safe() {
543        let v = parse(r#"{"a":"x","b":1}"#).expect("valid");
544        assert_eq!(v.get("a").and_then(Json::as_str), Some("x"));
545        assert_eq!(v.get("b").and_then(Json::as_str), None);
546        assert!(v.get("missing").is_none());
547        assert!(Json::Null.get("a").is_none());
548        assert!(Json::Null.as_str().is_none());
549    }
550
551    #[test]
552    fn object_builder_matches_parsed_form() {
553        let built =
554            Json::object(vec![("a", Json::str("x")), ("b", Json::Number(1.0))]);
555        assert_eq!(built.to_json(), r#"{"a":"x","b":1}"#);
556    }
557
558    #[test]
559    fn deep_nesting_does_not_overflow_the_stack() {
560        let depth = 200;
561        let src = format!("{}{}", "[".repeat(depth), "]".repeat(depth));
562        assert!(parse(&src).is_ok());
563    }
564}
565
566#[cfg(test)]
567mod escape_tests {
568    use super::*;
569
570    #[test]
571    fn every_escape_survives_a_round_trip() {
572        // Each of these has its own arm on both the reading and the
573        // writing side, and a mismatch between the two corrupts the
574        // document silently rather than failing.
575        for s in [
576            "back\\slash",
577            "carriage\rreturn",
578            "tab\there",
579            "quote\"inside",
580            "new\nline",
581            "all\\ \" \n \r \t together",
582        ] {
583            let encoded = Json::str(s).to_json();
584            assert_eq!(
585                parse(&encoded).expect("re-parses").as_str(),
586                Some(s),
587                "round trip failed for {s:?} via {encoded}"
588            );
589        }
590    }
591
592    #[test]
593    fn a_structural_error_names_what_was_expected() {
594        // A bare "invalid JSON" would give a client nothing to act on.
595        let e = parse(r#"{"a":1 "b":2}"#).expect_err("malformed");
596        assert!(e.contains(',') || e.contains('}'), "{e}");
597
598        let e = parse("[1 2]").expect_err("malformed");
599        assert!(e.contains(',') || e.contains(']'), "{e}");
600    }
601}