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//! This implementation stores numbers as `f64`; JSON itself does not
27//! require that representation. An integer beyond 2^53 does
28//! may lose precision. Callers requiring larger exact integers must use a
29//! different representation. The LSP transport uses this crate; MCP uses
30//! its SDK's JSON implementation.
31//!
32//! ```
33//! use oxml_json::{parse, Json};
34//!
35//! let value = parse(r#"{"jsonrpc":"2.0","id":1}"#).expect("valid JSON");
36//! assert_eq!(value.get("jsonrpc").and_then(Json::as_str), Some("2.0"));
37//! ```
38
39#![forbid(unsafe_code)]
40#![deny(missing_docs)]
41
42use std::collections::BTreeMap;
43use std::fmt::Write as _;
44
45/// A JSON value.
46#[derive(Debug, Clone, PartialEq)]
47pub enum Json {
48    /// `null`
49    Null,
50    /// `true` or `false`
51    Bool(bool),
52    /// A number.
53    Number(f64),
54    /// A string.
55    String(String),
56    /// An array.
57    Array(Vec<Json>),
58    /// An object. Ordered so output is deterministic.
59    Object(BTreeMap<String, Json>),
60}
61
62impl Json {
63    /// Build an object from pairs.
64    #[must_use]
65    pub fn object(pairs: Vec<(&str, Self)>) -> Self {
66        Self::Object(
67            pairs.into_iter().map(|(k, v)| (k.to_owned(), v)).collect(),
68        )
69    }
70
71    /// A string value.
72    #[must_use]
73    pub fn str(s: impl Into<String>) -> Self {
74        Self::String(s.into())
75    }
76
77    /// Look up a key on an object.
78    #[must_use]
79    pub fn get(&self, key: &str) -> Option<&Self> {
80        match self {
81            Self::Object(m) => m.get(key),
82            _ => None,
83        }
84    }
85
86    /// The value as a string slice, if it is one.
87    #[must_use]
88    pub fn as_str(&self) -> Option<&str> {
89        match self {
90            Self::String(s) => Some(s),
91            _ => None,
92        }
93    }
94
95    /// Serialise to JSON text.
96    #[must_use]
97    pub fn to_json(&self) -> String {
98        let mut out = String::new();
99        self.write(&mut out);
100        out
101    }
102
103    fn write(&self, out: &mut String) {
104        match self {
105            Self::Null => out.push_str("null"),
106            Self::Bool(b) => {
107                out.push_str(if *b { "true" } else { "false" });
108            }
109            Self::Number(n) => {
110                // JSON has one number type; printing an integral
111                // value as `2` rather than `2.0` is what every other
112                // encoder does, and the bound keeps the cast exact.
113                #[allow(clippy::cast_possible_truncation)]
114                if n.fract() == 0.0 && n.abs() < 1e15 {
115                    let _ = write!(out, "{}", *n as i64);
116                } else {
117                    let _ = write!(out, "{n}");
118                }
119            }
120            Self::String(s) => write_string(s, out),
121            Self::Array(items) => {
122                out.push('[');
123                for (i, item) in items.iter().enumerate() {
124                    if i > 0 {
125                        out.push(',');
126                    }
127                    item.write(out);
128                }
129                out.push(']');
130            }
131            Self::Object(map) => {
132                out.push('{');
133                for (i, (k, v)) in map.iter().enumerate() {
134                    if i > 0 {
135                        out.push(',');
136                    }
137                    write_string(k, out);
138                    out.push(':');
139                    v.write(out);
140                }
141                out.push('}');
142            }
143        }
144    }
145}
146
147fn write_string(s: &str, out: &mut String) {
148    out.push('"');
149    for c in s.chars() {
150        match c {
151            '"' => out.push_str("\\\""),
152            '\\' => out.push_str("\\\\"),
153            '\n' => out.push_str("\\n"),
154            '\r' => out.push_str("\\r"),
155            '\t' => out.push_str("\\t"),
156            // Control characters must be escaped or the output is not
157            // valid JSON, and XML documents do contain them.
158            c if (c as u32) < 0x20 => {
159                let _ = write!(out, "\\u{:04x}", c as u32);
160            }
161            c => out.push(c),
162        }
163    }
164    out.push('"');
165}
166
167/// Parse JSON text.
168///
169/// # Errors
170///
171/// Returns a description if the input is not valid JSON, exceeds 16 MiB,
172/// or contains more than 256 nested arrays and objects.
173pub fn parse(input: &str) -> Result<Json, String> {
174    if input.len() > 16 * 1024 * 1024 {
175        return Err("JSON source limit exceeded (16 MiB)".to_owned());
176    }
177    let chars: Vec<char> = input.chars().collect();
178    let mut p = Parser {
179        chars: &chars,
180        pos: 0,
181        depth: 0,
182    };
183    p.ws();
184    let v = p.value()?;
185    p.ws();
186    if p.pos < chars.len() {
187        return Err("trailing input after JSON value".to_owned());
188    }
189    Ok(v)
190}
191
192struct Parser<'a> {
193    chars: &'a [char],
194    pos: usize,
195    depth: usize,
196}
197
198impl Parser<'_> {
199    fn ws(&mut self) {
200        while matches!(self.chars.get(self.pos), Some(' ' | '\t' | '\n' | '\r'))
201        {
202            self.pos += 1;
203        }
204    }
205
206    fn value(&mut self) -> Result<Json, String> {
207        self.ws();
208        let container = matches!(self.chars.get(self.pos), Some('{' | '['));
209        if container && self.depth >= 256 {
210            return Err("JSON nesting limit exceeded (256)".to_owned());
211        }
212        self.depth += usize::from(container);
213        let result = self.value_inner();
214        self.depth -= usize::from(container);
215        result
216    }
217
218    fn value_inner(&mut self) -> Result<Json, String> {
219        match self.chars.get(self.pos) {
220            Some('{') => self.object(),
221            Some('[') => self.array(),
222            Some('"') => Ok(Json::String(self.string()?)),
223            Some('t') => self.literal("true", Json::Bool(true)),
224            Some('f') => self.literal("false", Json::Bool(false)),
225            Some('n') => self.literal("null", Json::Null),
226            Some(_) => self.number(),
227            None => Err("unexpected end of JSON".to_owned()),
228        }
229    }
230
231    fn literal(&mut self, word: &str, v: Json) -> Result<Json, String> {
232        if self.chars[self.pos..]
233            .starts_with(word.chars().collect::<Vec<_>>().as_slice())
234        {
235            self.pos += word.len();
236            Ok(v)
237        } else {
238            Err(format!("expected `{word}`"))
239        }
240    }
241
242    fn object(&mut self) -> Result<Json, String> {
243        self.pos += 1;
244        let mut map = BTreeMap::new();
245        self.ws();
246        if self.chars.get(self.pos) == Some(&'}') {
247            self.pos += 1;
248            return Ok(Json::Object(map));
249        }
250        loop {
251            self.ws();
252            let key = self.string()?;
253            self.ws();
254            if self.chars.get(self.pos) != Some(&':') {
255                return Err("expected `:` in object".to_owned());
256            }
257            self.pos += 1;
258            let value = self.value()?;
259            let _ = map.insert(key, value);
260            self.ws();
261            match self.chars.get(self.pos) {
262                Some(',') => self.pos += 1,
263                Some('}') => {
264                    self.pos += 1;
265                    return Ok(Json::Object(map));
266                }
267                _ => return Err("expected `,` or `}`".to_owned()),
268            }
269        }
270    }
271
272    fn array(&mut self) -> Result<Json, String> {
273        self.pos += 1;
274        let mut items = Vec::new();
275        self.ws();
276        if self.chars.get(self.pos) == Some(&']') {
277            self.pos += 1;
278            return Ok(Json::Array(items));
279        }
280        loop {
281            items.push(self.value()?);
282            self.ws();
283            match self.chars.get(self.pos) {
284                Some(',') => self.pos += 1,
285                Some(']') => {
286                    self.pos += 1;
287                    return Ok(Json::Array(items));
288                }
289                _ => return Err("expected `,` or `]`".to_owned()),
290            }
291        }
292    }
293
294    fn string(&mut self) -> Result<String, String> {
295        if self.chars.get(self.pos) != Some(&'"') {
296            return Err("expected a string".to_owned());
297        }
298        self.pos += 1;
299        let mut out = String::new();
300        loop {
301            match self.chars.get(self.pos) {
302                None => return Err("unterminated string".to_owned()),
303                Some('"') => {
304                    self.pos += 1;
305                    return Ok(out);
306                }
307                Some('\\') => {
308                    self.pos += 1;
309                    out.push(self.escape()?);
310                }
311                Some(c) if *c < '\u{20}' => {
312                    return Err("unescaped control in string".to_owned());
313                }
314                Some(c) => {
315                    out.push(*c);
316                    self.pos += 1;
317                }
318            }
319        }
320    }
321
322    fn escape(&mut self) -> Result<char, String> {
323        let esc = *self.chars.get(self.pos).ok_or("trailing backslash")?;
324        self.pos += 1;
325        match esc {
326            'u' => self.unicode_escape(),
327            _ => simple_escape(esc)
328                .ok_or_else(|| "invalid string escape".to_owned()),
329        }
330    }
331
332    fn unicode_escape(&mut self) -> Result<char, String> {
333        let cp = self.hex4()?;
334        // A non-BMP character is escaped as a surrogate *pair*; neither
335        // half is a `char` on its own. Python's `json.dumps` escapes
336        // non-ASCII by default, so a client sending an emoji arrives
337        // this way.
338        let cp = if (0xD800..0xDC00).contains(&cp) {
339            if !matches!(
340                self.chars.get(self.pos..self.pos + 2),
341                Some(['\\', 'u'])
342            ) {
343                return Err("lone high surrogate".to_owned());
344            }
345            self.pos += 2;
346            let lo = self.hex4()?;
347            if !(0xDC00..0xE000).contains(&lo) {
348                return Err(
349                    "high surrogate not followed by a low surrogate".to_owned()
350                );
351            }
352            0x1_0000 + ((cp - 0xD800) << 10) + (lo - 0xDC00)
353        } else if (0xDC00..0xE000).contains(&cp) {
354            return Err("lone low surrogate".to_owned());
355        } else {
356            cp
357        };
358        char::from_u32(cp).ok_or_else(|| "bad code point".to_owned())
359    }
360
361    /// Read exactly four hex digits of a `\\u` escape.
362    fn hex4(&mut self) -> Result<u32, String> {
363        let hex: String = self
364            .chars
365            .get(self.pos..self.pos + 4)
366            .ok_or("truncated \\u escape")?
367            .iter()
368            .collect();
369        self.pos += 4;
370        u32::from_str_radix(&hex, 16).map_err(|_| "bad \\u escape".to_owned())
371    }
372
373    fn number(&mut self) -> Result<Json, String> {
374        let start = self.pos;
375        while matches!(
376            self.chars.get(self.pos),
377            Some(c) if c.is_ascii_digit()
378                || matches!(c, '-' | '+' | '.' | 'e' | 'E')
379        ) {
380            self.pos += 1;
381        }
382        let text: String = self.chars[start..self.pos].iter().collect();
383        if !valid_number(&text) {
384            return Err(format!("invalid number `{text}`"));
385        }
386        let value: f64 = text
387            .parse()
388            .map_err(|_| format!("invalid number `{text}`"))?;
389        // `"1e999"` is grammatically valid JSON and parses to
390        // infinity, which JSON cannot express -- so serialising it
391        // again produced output that would not parse. Refusing it here
392        // is the honest answer for a parser that carries numbers as
393        // `f64`: the alternative, writing `null`, silently changes the
394        // value.
395        //
396        // Found by fuzzing, on `-3e805`, within ninety seconds of the
397        // round-trip assertion being added.
398        if !value.is_finite() {
399            return Err(format!("number `{text}` is out of range for f64"));
400        }
401        Ok(Json::Number(value))
402    }
403}
404
405fn simple_escape(escaped: char) -> Option<char> {
406    match escaped {
407        'n' => Some('\n'),
408        't' => Some('\t'),
409        'r' => Some('\r'),
410        'b' => Some('\u{8}'),
411        'f' => Some('\u{c}'),
412        c if ['"', '\\', '/'].contains(&c) => Some(c),
413        _ => None,
414    }
415}
416
417fn digits(text: &str) -> bool {
418    !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())
419}
420
421fn valid_mantissa(text: &str) -> bool {
422    let (integer, fraction) = text
423        .split_once('.')
424        .map_or((text, None), |(integer, fraction)| {
425            (integer, Some(fraction))
426        });
427    digits(integer)
428        && !(integer.len() > 1 && integer.starts_with('0'))
429        && fraction.is_none_or(digits)
430}
431
432fn valid_number(text: &str) -> bool {
433    let unsigned = text.strip_prefix('-').unwrap_or(text);
434    let mut parts = unsigned.split(['e', 'E']);
435    let mantissa = parts.next().unwrap_or_default();
436    let exponent = parts.next();
437    parts.next().is_none()
438        && valid_mantissa(mantissa)
439        && exponent.is_none_or(|value| {
440            let unsigned = value
441                .strip_prefix('+')
442                .or_else(|| value.strip_prefix('-'))
443                .unwrap_or(value);
444            digits(unsigned)
445        })
446}
447
448#[cfg(test)]
449#[path = "tests/basic.rs"]
450mod tests;
451
452#[cfg(test)]
453#[path = "tests/escapes.rs"]
454mod escape_tests;