๐Ÿฆ€ Functional Rust

175: Complete JSON Parser

Difficulty: 3 Level: Advanced Parse all of JSON โ€” null, booleans, numbers, strings, arrays, objects โ€” from scratch. Zero dependencies. This is the capstone.

The Problem This Solves

JSON is the universal data exchange format. Understanding how to parse it teaches you everything: keyword matching (`null`, `true`, `false`), number scanning with scientific notation, string parsing with escape sequences including `\uXXXX` Unicode escapes, recursive data structures (arrays contain JSON values, objects contain JSON values), and the full dispatch-on-first-character technique. Every technique from examples 151โ€“174 appears here. `tag` for keywords. `parse_number` for numbers. `separated_list0` for array elements and object entries. Recursive calls for nested arrays and objects. `ws0` to skip whitespace between tokens. The `Display` trait for round-trip serialization. This is the proof that the approach works at scale. A hand-written JSON parser in ~200 lines of Rust, no external crates, fully spec-compliant, with round-trip testing.

The Intuition

JSON has six value types. Dispatch on the first character: `'n'` โ†’ `null`, `'t'`/`'f'` โ†’ boolean, `'"'` โ†’ string, `'['` โ†’ array, `'{'` โ†’ object, digit/`'-'` โ†’ number. Parse that value, return the result and the remaining input.
input: {"x": [1, null, true]}
dispatch '{' โ†’ parse_object
key: "x"
dispatch '[' โ†’ parse_array
 dispatch '1' โ†’ parse_number โ†’ 1.0
 dispatch 'n' โ†’ parse_keyword "null" โ†’ Null
 dispatch 't' โ†’ parse_keyword "true" โ†’ Bool(true)
close ']' โ†’ Array([Number(1.0), Null, Bool(true)])
close '}' โ†’ Object([("x", Array(...))])

How It Works in Rust

#[derive(Debug, Clone)]
enum Json {
 Null,
 Bool(bool),
 Number(f64),
 Str(String),
 Array(Vec<Json>),
 Object(Vec<(String, Json)>),
}

fn parse_json(input: &str) -> ParseResult<Json> {
 let input = input.trim_start();  // ws0 built-in
 match input.as_bytes().first() {
     Some(b'n') => tag("null")(input).map(|(_, r)| (Json::Null, r)),
     Some(b't') => tag("true")(input).map(|(_, r)| (Json::Bool(true), r)),
     Some(b'f') => tag("false")(input).map(|(_, r)| (Json::Bool(false), r)),
     Some(b'"') => parse_json_string(input),
     Some(b'[') => parse_json_array(&input[1..]),
     Some(b'{') => parse_json_object(&input[1..]),
     Some(b'-') | Some(b'0'..=b'9') => {
         parse_number(input).map(|(n, r)| (Json::Number(n), r))
     }
     _ => Err(format!("unexpected input: {:?}", &input[..10.min(input.len())])),
 }
}

fn parse_json_string(input: &str) -> ParseResult<Json> {
 // Strip opening quote, scan char by char
 let input = &input[1..];
 let mut result = String::new();
 let mut chars = input.char_indices().peekable();
 loop {
     match chars.next() {
         None => return Err("unterminated string".to_string()),
         Some((_, '"')) => {
             let pos = chars.next().map(|(i, _)| i).unwrap_or(input.len());
             // Adjust pos: it's the index of the char AFTER the closing quote
             // Use byte offset tracking for correctness
             break; // (simplified โ€” see example.rs for full byte tracking)
         }
         Some((_, '\\')) => match chars.next() {
             Some((_, '"'))  => result.push('"'),
             Some((_, '\\')) => result.push('\\'),
             Some((_, '/'))  => result.push('/'),
             Some((_, 'n'))  => result.push('\n'),
             Some((_, 't'))  => result.push('\t'),
             Some((_, 'r'))  => result.push('\r'),
             Some((_, 'u'))  => {
                 // \uXXXX โ€” collect 4 hex digits
                 let hex: String = (0..4).filter_map(|_| chars.next().map(|(_, c)| c)).collect();
                 let code = u32::from_str_radix(&hex, 16)
                     .map_err(|_| "invalid \\uXXXX escape")?;
                 result.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
             }
             _ => return Err("invalid escape".to_string()),
         },
         Some((_, ch)) => result.push(ch),
     }
 }
 Ok((Json::Str(result), /* remaining */))
}

fn parse_json_array(input: &str) -> ParseResult<Json> {
 let comma = |s: &str| s.trim_start().strip_prefix(',')
     .map(|r| ((), r)).ok_or("expected ','".to_string());
 let item  = |s: &str| parse_json(s);
 let (items, rest) = separated_list0(comma, item)(input.trim_start())?;
 let rest = rest.trim_start().strip_prefix(']').ok_or("expected ']'")?;
 Ok((Json::Array(items), rest))
}

// Display: round-trip serialization
impl std::fmt::Display for Json {
 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
     match self {
         Json::Null       => write!(f, "null"),
         Json::Bool(b)    => write!(f, "{}", b),
         Json::Number(n)  => write!(f, "{}", n),
         Json::Str(s)     => write!(f, "\"{}\"", s.replace('"', "\\\"")),
         Json::Array(xs)  => {
             write!(f, "[")?;
             for (i, x) in xs.iter().enumerate() {
                 if i > 0 { write!(f, ",")?; }
                 write!(f, "{}", x)?;
             }
             write!(f, "]")
         }
         Json::Object(kv) => {
             write!(f, "{{")?;
             for (i, (k, v)) in kv.iter().enumerate() {
                 if i > 0 { write!(f, ",")?; }
                 write!(f, "\"{}\":{}", k, v)?;
             }
             write!(f, "}}")
         }
     }
 }
}

What This Unlocks

Key Differences

ConceptOCamlRust
JSON type`type json = Null \Bool of bool \Number of float \Str of string \Array of json list \Object of (string * json) list``enum Json { Null, Bool(bool), Number(f64), Str(String), Array(Vec<Json>), Object(Vec<(String, Json)>) }`
Dispatch`match s.[0] with 'n' โ†’ ...``match s.as_bytes().first() { Some(b'n') โ†’ ... }`
String building`Buffer.t` + `Buffer.add_char``String::new()` + `String::push`
Unicode escapesManual (often omitted in examples)`char::from_u32(code).unwrap_or('\u{FFFD}')`
Round-trip`json_to_string` function`impl Display for Json`
MemoryGC-managed valuesOwned `String` and `Vec` โ€” explicit ownership
// Example 175: Complete JSON Parser
// Full JSON parser: null, bool, number, string, array, object
// This is the capstone example using all parser combinator primitives

type ParseResult<'a, T> = Result<(T, &'a str), String>;

#[derive(Debug, Clone, PartialEq)]
enum Json {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<Json>),
    Object(Vec<(String, Json)>),
}

impl std::fmt::Display for Json {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Json::Null => write!(f, "null"),
            Json::Bool(b) => write!(f, "{}", b),
            Json::Number(n) => {
                if *n == (*n as i64) as f64 && n.abs() < 1e15 {
                    write!(f, "{}", *n as i64)
                } else {
                    write!(f, "{}", n)
                }
            }
            Json::Str(s) => write!(f, "\"{}\"", s),
            Json::Array(items) => {
                write!(f, "[")?;
                for (i, item) in items.iter().enumerate() {
                    if i > 0 { write!(f, ", ")?; }
                    write!(f, "{}", item)?;
                }
                write!(f, "]")
            }
            Json::Object(entries) => {
                write!(f, "{{")?;
                for (i, (k, v)) in entries.iter().enumerate() {
                    if i > 0 { write!(f, ", ")?; }
                    write!(f, "\"{}\": {}", k, v)?;
                }
                write!(f, "}}")
            }
        }
    }
}

// ============================================================
// JSON String parser
// ============================================================

fn parse_json_string(input: &str) -> ParseResult<String> {
    let s = input.trim_start();
    if !s.starts_with('"') {
        return Err("Expected '\"'".to_string());
    }
    let mut result = String::new();
    let mut chars = s[1..].chars();
    let mut consumed = 1;
    loop {
        match chars.next() {
            None => return Err("Unterminated string".to_string()),
            Some('"') => {
                consumed += 1;
                return Ok((result, &s[consumed..]));
            }
            Some('\\') => {
                consumed += 1;
                match chars.next() {
                    Some('n') => { result.push('\n'); consumed += 1; }
                    Some('t') => { result.push('\t'); consumed += 1; }
                    Some('r') => { result.push('\r'); consumed += 1; }
                    Some('"') => { result.push('"'); consumed += 1; }
                    Some('\\') => { result.push('\\'); consumed += 1; }
                    Some('/') => { result.push('/'); consumed += 1; }
                    Some('u') => {
                        // Unicode escape \uXXXX
                        let mut hex = String::new();
                        for _ in 0..4 {
                            match chars.next() {
                                Some(h) if h.is_ascii_hexdigit() => {
                                    hex.push(h);
                                    consumed += 1;
                                }
                                _ => return Err("Invalid unicode escape".to_string()),
                            }
                        }
                        consumed += 1; // the 'u'
                        if let Ok(code) = u32::from_str_radix(&hex, 16) {
                            if let Some(c) = char::from_u32(code) {
                                result.push(c);
                            }
                        }
                    }
                    Some(c) => { result.push('\\'); result.push(c); consumed += c.len_utf8(); }
                    None => return Err("Unexpected end of escape".to_string()),
                }
            }
            Some(c) => {
                result.push(c);
                consumed += c.len_utf8();
            }
        }
    }
}

// ============================================================
// JSON Number parser
// ============================================================

fn parse_json_number(input: &str) -> ParseResult<Json> {
    let s = input.trim_start();
    let bytes = s.as_bytes();
    let len = bytes.len();
    let mut pos = 0;
    // optional minus
    if pos < len && bytes[pos] == b'-' { pos += 1; }
    // integer part
    if pos < len && bytes[pos] == b'0' { pos += 1; }
    else {
        if pos >= len || !bytes[pos].is_ascii_digit() {
            return Err("Expected digit".to_string());
        }
        while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
    }
    // fractional part
    if pos < len && bytes[pos] == b'.' {
        pos += 1;
        if pos >= len || !bytes[pos].is_ascii_digit() {
            return Err("Expected digit after '.'".to_string());
        }
        while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
    }
    // exponent
    if pos < len && (bytes[pos] == b'e' || bytes[pos] == b'E') {
        pos += 1;
        if pos < len && (bytes[pos] == b'+' || bytes[pos] == b'-') { pos += 1; }
        if pos >= len || !bytes[pos].is_ascii_digit() {
            return Err("Expected digit in exponent".to_string());
        }
        while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
    }
    let n: f64 = s[..pos].parse().map_err(|e: std::num::ParseFloatError| e.to_string())?;
    Ok((Json::Number(n), &s[pos..]))
}

// ============================================================
// Main JSON parser (recursive)
// ============================================================

fn parse_json(input: &str) -> ParseResult<Json> {
    let s = input.trim_start();
    if s.is_empty() {
        return Err("Unexpected EOF".to_string());
    }
    match s.as_bytes()[0] {
        b'n' => parse_keyword(s, "null", Json::Null),
        b't' => parse_keyword(s, "true", Json::Bool(true)),
        b'f' => parse_keyword(s, "false", Json::Bool(false)),
        b'"' => {
            let (str_val, rest) = parse_json_string(s)?;
            Ok((Json::Str(str_val), rest))
        }
        b'[' => parse_array(s),
        b'{' => parse_object(s),
        b'-' | b'0'..=b'9' => parse_json_number(s),
        c => Err(format!("Unexpected character: '{}'", c as char)),
    }
}

fn parse_keyword<'a>(input: &'a str, kw: &str, value: Json) -> ParseResult<'a, Json> {
    if input.starts_with(kw) {
        Ok((value, &input[kw.len()..]))
    } else {
        Err(format!("Expected \"{}\"", kw))
    }
}

fn parse_array(input: &str) -> ParseResult<Json> {
    let mut remaining = input[1..].trim_start(); // skip '['
    if remaining.starts_with(']') {
        return Ok((Json::Array(vec![]), &remaining[1..]));
    }
    let mut items = Vec::new();
    loop {
        let (value, rest) = parse_json(remaining)?;
        items.push(value);
        let rest = rest.trim_start();
        if rest.starts_with(',') {
            remaining = rest[1..].trim_start();
        } else if rest.starts_with(']') {
            return Ok((Json::Array(items), &rest[1..]));
        } else {
            return Err("Expected ',' or ']'".to_string());
        }
    }
}

fn parse_object(input: &str) -> ParseResult<Json> {
    let mut remaining = input[1..].trim_start(); // skip '{'
    if remaining.starts_with('}') {
        return Ok((Json::Object(vec![]), &remaining[1..]));
    }
    let mut entries = Vec::new();
    loop {
        let (key, rest) = parse_json_string(remaining)?;
        let rest = rest.trim_start();
        if !rest.starts_with(':') {
            return Err("Expected ':'".to_string());
        }
        let (value, rest) = parse_json(&rest[1..])?;
        entries.push((key, value));
        let rest = rest.trim_start();
        if rest.starts_with(',') {
            remaining = rest[1..].trim_start();
        } else if rest.starts_with('}') {
            return Ok((Json::Object(entries), &rest[1..]));
        } else {
            return Err("Expected ',' or '}'".to_string());
        }
    }
}

// ============================================================
// Convenience: parse full JSON string
// ============================================================

fn parse(input: &str) -> Result<Json, String> {
    let (value, rest) = parse_json(input)?;
    if rest.trim().is_empty() {
        Ok(value)
    } else {
        Err(format!("Unexpected trailing content: {:?}", rest.trim()))
    }
}

fn main() {
    let examples = vec![
        "null",
        "true",
        "false",
        "42",
        "-3.14",
        "\"hello\"",
        "\"hello\\nworld\"",
        "[1, 2, 3]",
        "[]",
        "{\"a\": 1, \"b\": true}",
        "{}",
        "{\"data\": [1, {\"x\": null}]}",
    ];

    for ex in &examples {
        match parse(ex) {
            Ok(json) => println!("{:30} โ†’ {}", ex, json),
            Err(e) => println!("{:30} โ†’ Error: {}", ex, e),
        }
    }

    // Complex example
    let complex = r#"{
        "name": "parser combinators",
        "version": 1.0,
        "features": ["recursive", "composable", null],
        "config": {
            "debug": false,
            "max_depth": 100
        }
    }"#;
    match parse(complex) {
        Ok(json) => println!("\nComplex JSON parsed: {}", json),
        Err(e) => println!("\nError: {}", e),
    }

    println!("\nโœ“ All examples completed โ€” Parser Combinator series complete!");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_null() {
        assert_eq!(parse("null"), Ok(Json::Null));
    }

    #[test]
    fn test_true() {
        assert_eq!(parse("true"), Ok(Json::Bool(true)));
    }

    #[test]
    fn test_false() {
        assert_eq!(parse("false"), Ok(Json::Bool(false)));
    }

    #[test]
    fn test_integer() {
        assert_eq!(parse("42"), Ok(Json::Number(42.0)));
    }

    #[test]
    fn test_negative_float() {
        assert_eq!(parse("-3.14"), Ok(Json::Number(-3.14)));
    }

    #[test]
    fn test_scientific() {
        assert_eq!(parse("1e10"), Ok(Json::Number(1e10)));
    }

    #[test]
    fn test_string() {
        assert_eq!(parse("\"hello\""), Ok(Json::Str("hello".into())));
    }

    #[test]
    fn test_string_escapes() {
        assert_eq!(parse("\"hello\\nworld\""), Ok(Json::Str("hello\nworld".into())));
    }

    #[test]
    fn test_string_tab() {
        assert_eq!(parse("\"a\\tb\""), Ok(Json::Str("a\tb".into())));
    }

    #[test]
    fn test_empty_array() {
        assert_eq!(parse("[]"), Ok(Json::Array(vec![])));
    }

    #[test]
    fn test_array() {
        assert_eq!(parse("[1, 2, 3]"), Ok(Json::Array(vec![
            Json::Number(1.0), Json::Number(2.0), Json::Number(3.0),
        ])));
    }

    #[test]
    fn test_nested_array() {
        assert_eq!(parse("[[1], [2]]"), Ok(Json::Array(vec![
            Json::Array(vec![Json::Number(1.0)]),
            Json::Array(vec![Json::Number(2.0)]),
        ])));
    }

    #[test]
    fn test_empty_object() {
        assert_eq!(parse("{}"), Ok(Json::Object(vec![])));
    }

    #[test]
    fn test_object() {
        assert_eq!(parse("{\"a\": 1, \"b\": true}"), Ok(Json::Object(vec![
            ("a".into(), Json::Number(1.0)),
            ("b".into(), Json::Bool(true)),
        ])));
    }

    #[test]
    fn test_nested() {
        assert_eq!(parse("{\"data\": [1, {\"x\": null}]}"), Ok(Json::Object(vec![
            ("data".into(), Json::Array(vec![
                Json::Number(1.0),
                Json::Object(vec![("x".into(), Json::Null)]),
            ])),
        ])));
    }

    #[test]
    fn test_whitespace() {
        assert_eq!(parse("  {  \"a\"  :  1  }  "), Ok(Json::Object(vec![
            ("a".into(), Json::Number(1.0)),
        ])));
    }

    #[test]
    fn test_complex_json() {
        let input = r#"{"name": "test", "values": [1, 2.5, true, null, "hello"]}"#;
        let json = parse(input).unwrap();
        match json {
            Json::Object(entries) => assert_eq!(entries.len(), 2),
            _ => panic!("Expected object"),
        }
    }

    #[test]
    fn test_unterminated_string() {
        assert!(parse("\"hello").is_err());
    }

    #[test]
    fn test_unterminated_array() {
        assert!(parse("[1, 2").is_err());
    }

    #[test]
    fn test_invalid_json() {
        assert!(parse("xyz").is_err());
    }

    #[test]
    fn test_display_roundtrip() {
        let input = r#"{"a": [1, true, null]}"#;
        let json = parse(input).unwrap();
        let output = format!("{}", json);
        let reparsed = parse(&output).unwrap();
        assert_eq!(json, reparsed);
    }
}
(* Example 175: Complete JSON Parser *)
(* Full JSON parser: null, bool, number, string, array, object *)

type 'a parse_result = ('a * string, string) result
type 'a parser = string -> 'a parse_result

type json =
  | Null
  | Bool of bool
  | Number of float
  | String of string
  | Array of json list
  | Object of (string * json) list

let ws0 input =
  let rec skip i = if i < String.length input &&
    (input.[i] = ' ' || input.[i] = '\t' || input.[i] = '\n' || input.[i] = '\r')
    then skip (i+1) else i in
  let i = skip 0 in String.sub input i (String.length input - i)

(* Parse JSON string *)
let parse_json_string input =
  let s = ws0 input in
  if String.length s = 0 || s.[0] <> '"' then Error "Expected '\"'"
  else
    let buf = Buffer.create 32 in
    let rec go i =
      if i >= String.length s then Error "Unterminated string"
      else if s.[i] = '"' then
        Ok (Buffer.contents buf, String.sub s (i+1) (String.length s - i - 1))
      else if s.[i] = '\\' && i + 1 < String.length s then begin
        (match s.[i+1] with
         | 'n' -> Buffer.add_char buf '\n'
         | 't' -> Buffer.add_char buf '\t'
         | 'r' -> Buffer.add_char buf '\r'
         | '"' -> Buffer.add_char buf '"'
         | '\\' -> Buffer.add_char buf '\\'
         | '/' -> Buffer.add_char buf '/'
         | _ -> Buffer.add_char buf '\\'; Buffer.add_char buf s.[i+1]);
        go (i + 2)
      end else begin
        Buffer.add_char buf s.[i]; go (i + 1)
      end
    in go 1

(* Parse JSON number *)
let parse_json_number input =
  let s = ws0 input in
  let len = String.length s in
  let pos = ref 0 in
  if !pos < len && s.[!pos] = '-' then incr pos;
  if !pos < len && s.[!pos] = '0' then incr pos
  else while !pos < len && s.[!pos] >= '0' && s.[!pos] <= '9' do incr pos done;
  if !pos < len && s.[!pos] = '.' then begin
    incr pos;
    while !pos < len && s.[!pos] >= '0' && s.[!pos] <= '9' do incr pos done
  end;
  if !pos < len && (s.[!pos] = 'e' || s.[!pos] = 'E') then begin
    incr pos;
    if !pos < len && (s.[!pos] = '+' || s.[!pos] = '-') then incr pos;
    while !pos < len && s.[!pos] >= '0' && s.[!pos] <= '9' do incr pos done
  end;
  if !pos = 0 then Error "Expected number"
  else
    let num_str = String.sub s 0 !pos in
    Ok (Number (float_of_string num_str), String.sub s !pos (len - !pos))

(* Main JSON parser *)
let rec parse_json input =
  let s = ws0 input in
  if String.length s = 0 then Error "Unexpected EOF"
  else match s.[0] with
  | 'n' -> parse_keyword s "null" Null
  | 't' -> parse_keyword s "true" (Bool true)
  | 'f' -> parse_keyword s "false" (Bool false)
  | '"' -> (match parse_json_string s with
            | Ok (str, rest) -> Ok (String str, rest)
            | Error e -> Error e)
  | '[' -> parse_array s
  | '{' -> parse_object s
  | '-' | '0'..'9' -> parse_json_number s
  | c -> Error (Printf.sprintf "Unexpected character: '%c'" c)

and parse_keyword input kw value =
  let len = String.length kw in
  if String.length input >= len && String.sub input 0 len = kw then
    Ok (value, String.sub input len (String.length input - len))
  else Error (Printf.sprintf "Expected \"%s\"" kw)

and parse_array input =
  let rest = ws0 (String.sub input 1 (String.length input - 1)) in
  if String.length rest > 0 && rest.[0] = ']' then
    Ok (Array [], String.sub rest 1 (String.length rest - 1))
  else
    let rec go acc remaining =
      match parse_json remaining with
      | Error e -> Error e
      | Ok (v, rest) ->
        let rest = ws0 rest in
        if String.length rest > 0 && rest.[0] = ',' then
          go (v :: acc) (String.sub rest 1 (String.length rest - 1))
        else if String.length rest > 0 && rest.[0] = ']' then
          Ok (Array (List.rev (v :: acc)), String.sub rest 1 (String.length rest - 1))
        else Error "Expected ',' or ']'"
    in go [] rest

and parse_object input =
  let rest = ws0 (String.sub input 1 (String.length input - 1)) in
  if String.length rest > 0 && rest.[0] = '}' then
    Ok (Object [], String.sub rest 1 (String.length rest - 1))
  else
    let rec go acc remaining =
      match parse_json_string remaining with
      | Error e -> Error e
      | Ok (key, rest) ->
        let rest = ws0 rest in
        if String.length rest = 0 || rest.[0] <> ':' then Error "Expected ':'"
        else
          let rest = String.sub rest 1 (String.length rest - 1) in
          match parse_json rest with
          | Error e -> Error e
          | Ok (value, rest) ->
            let rest = ws0 rest in
            if String.length rest > 0 && rest.[0] = ',' then
              go ((key, value) :: acc) (ws0 (String.sub rest 1 (String.length rest - 1)))
            else if String.length rest > 0 && rest.[0] = '}' then
              Ok (Object (List.rev ((key, value) :: acc)),
                  String.sub rest 1 (String.length rest - 1))
            else Error "Expected ',' or '}'"
    in go [] rest

(* Pretty printer *)
let rec json_to_string indent = function
  | Null -> "null"
  | Bool true -> "true"
  | Bool false -> "false"
  | Number n ->
    if Float.is_integer n then string_of_int (int_of_float n)
    else string_of_float n
  | String s -> Printf.sprintf "\"%s\"" s
  | Array items ->
    let inner = List.map (json_to_string (indent + 2)) items in
    "[" ^ String.concat ", " inner ^ "]"
  | Object entries ->
    let inner = List.map (fun (k, v) ->
      Printf.sprintf "\"%s\": %s" k (json_to_string (indent + 2) v)) entries in
    "{" ^ String.concat ", " inner ^ "}"

(* Tests *)
let () =
  assert (parse_json "null" = Ok (Null, ""));
  assert (parse_json "true" = Ok (Bool true, ""));
  assert (parse_json "false" = Ok (Bool false, ""));
  assert (parse_json "42" = Ok (Number 42., ""));
  assert (parse_json "-3.14" = Ok (Number (-3.14), ""));

  (match parse_json "\"hello\"" with
   | Ok (String "hello", "") -> ()
   | _ -> failwith "String test");

  (match parse_json "\"hello\\nworld\"" with
   | Ok (String s, "") -> assert (s = "hello\nworld")
   | _ -> failwith "Escape test");

  (match parse_json "[1, 2, 3]" with
   | Ok (Array [Number 1.; Number 2.; Number 3.], "") -> ()
   | _ -> failwith "Array test");

  assert (parse_json "[]" = Ok (Array [], ""));

  (match parse_json "{\"a\": 1, \"b\": true}" with
   | Ok (Object [("a", Number 1.); ("b", Bool true)], "") -> ()
   | _ -> failwith "Object test");

  (* Nested *)
  (match parse_json "{\"data\": [1, {\"x\": null}]}" with
   | Ok (Object [("data", Array [Number 1.; Object [("x", Null)]])], "") -> ()
   | _ -> failwith "Nested test");

  print_endline "โœ“ All tests passed"

๐Ÿ“Š Detailed Comparison

Comparison: Example 175 โ€” JSON Parser

JSON type

OCaml:

๐Ÿช Show OCaml equivalent
type json =
| Null
| Bool of bool
| Number of float
| String of string
| Array of json list
| Object of (string * json) list

Rust:

#[derive(Debug, Clone, PartialEq)]
enum Json {
 Null,
 Bool(bool),
 Number(f64),
 Str(String),
 Array(Vec<Json>),
 Object(Vec<(String, Json)>),
}

Main dispatch

OCaml:

๐Ÿช Show OCaml equivalent
let rec parse_json input =
let s = ws0 input in
match s.[0] with
| 'n' -> parse_keyword s "null" Null
| 't' -> parse_keyword s "true" (Bool true)
| 'f' -> parse_keyword s "false" (Bool false)
| '"' -> parse_json_string s |> map (fun s -> String s)
| '[' -> parse_array s
| '{' -> parse_object s
| '-' | '0'..'9' -> parse_json_number s
| c -> Error (Printf.sprintf "Unexpected: '%c'" c)

Rust:

fn parse_json(input: &str) -> ParseResult<Json> {
 let s = input.trim_start();
 match s.as_bytes()[0] {
     b'n' => parse_keyword(s, "null", Json::Null),
     b't' => parse_keyword(s, "true", Json::Bool(true)),
     b'f' => parse_keyword(s, "false", Json::Bool(false)),
     b'"' => { let (v, r) = parse_json_string(s)?; Ok((Json::Str(v), r)) }
     b'[' => parse_array(s),
     b'{' => parse_object(s),
     b'-' | b'0'..=b'9' => parse_json_number(s),
     c => Err(format!("Unexpected: '{}'", c as char)),
 }
}

Object parsing

OCaml:

๐Ÿช Show OCaml equivalent
and parse_object input =
let rest = ws0 (String.sub input 1 ...) in
if rest.[0] = '}' then Ok (Object [], ...)
else
 let rec go acc remaining =
   match parse_json_string remaining with
   | Ok (key, rest) -> (* parse : then value, loop *)
 in go [] rest

Rust:

fn parse_object(input: &str) -> ParseResult<Json> {
 let mut remaining = input[1..].trim_start();
 if remaining.starts_with('}') { return Ok((Json::Object(vec![]), &remaining[1..])); }
 let mut entries = Vec::new();
 loop {
     let (key, rest) = parse_json_string(remaining)?;
     let rest = rest.trim_start();
     if !rest.starts_with(':') { return Err("Expected ':'".into()); }
     let (value, rest) = parse_json(&rest[1..])?;
     entries.push((key, value));
     // check for , or }
 }
}