Chris Collins

Lab · Playground

A tiny JSON parser in thirteen languages

A recursive-descent JSON parser and pretty-printer, written from scratch in thirteen languages — how each one represents "a value that could be anything", reports errors, and handles strings, escapes and Unicode.

Code blocks on this page are editable — change them and press Run.

Every language has a JSON library, and none of these use it. A JSON parser is the classic small recursive-descent parser: one function per kind of value, each calling value again for whatever is nested inside. Writing one exercises the parts of a language that FizzBuzz never touches:

  • a value that could be anything — null, a boolean, a number, a string, a list or an object — which is a sum type, a tagged union, or a dynamic language’s ordinary variable;

  • strings: walking them character by character, escapes like \" and \n, and turning \u00e9 into a real é (which means UTF-8, for the languages whose strings are bytes);

  • errors: what to do, deep inside the recursion, when the input is wrong;

  • keeping an object’s keys in order, which not every language’s map does.

Each block parses the same document, prints it back out indented by two spaces (as JSON.stringify(value, null, 2) would), counts the strings and numbers in it, adds up the numbers, and measures how deeply it nests. Then it tries three broken documents and reports where each goes wrong — the position is a zero-based character offset. All thirteen print exactly the same thing.

The parsers follow the grammar at json.org closely but not pedantically: numbers are handed to the language’s own float parser, \u escapes outside the Basic Multilingual Plane (surrogate pairs) are not joined up, and duplicate keys are not checked. Things to try: break the document in other ways and see whether the error makes sense; add "pi": 3.14159; or add a surrogate pair ("\ud83d\ude00") and see which languages cope.

Dynamic languages

JavaScript
const DOC = String.raw`{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}`;

class ParseError extends Error {}

function parse(text) {
  let i = 0;
  const fail = msg => { throw new ParseError(msg); };
  const ws = () => { while (i < text.length && ' \t\n\r'.includes(text[i])) i++; };
  const expect = c => { ws(); if (text[i] !== c) fail(`expected '${c}' at ${i}`); i++; };

  function value() {
    ws();
    if (i >= text.length) fail('unexpected end of input');
    const c = text[i];
    if (c === '{') return object();
    if (c === '[') return array();
    if (c === '"') return string();
    if (c === '-' || (c >= '0' && c <= '9')) return number();
    for (const [word, v] of [['true', true], ['false', false], ['null', null]])
      if (text.startsWith(word, i)) { i += word.length; return v; }
    fail(`unexpected '${c}' at ${i}`);
  }
  function object() {
    const pairs = new Map();
    i++; ws();
    if (text[i] === '}') { i++; return pairs; }
    for (;;) {
      ws();
      if (text[i] !== '"') fail(`expected string at ${i}`);
      const key = string();
      expect(':');
      pairs.set(key, value());
      ws();
      if (text[i] === ',') { i++; continue; }
      if (text[i] === '}') { i++; return pairs; }
      fail(`expected ',' or '}' at ${i}`);
    }
  }
  function array() {
    const items = [];
    i++; ws();
    if (text[i] === ']') { i++; return items; }
    for (;;) {
      items.push(value());
      ws();
      if (text[i] === ',') { i++; continue; }
      if (text[i] === ']') { i++; return items; }
      fail(`expected ',' or ']' at ${i}`);
    }
  }
  function string() {
    let out = '';
    i++;
    for (;;) {
      if (i >= text.length) fail('unexpected end of input');
      const c = text[i++];
      if (c === '"') return out;
      if (c !== '\\') { out += c; continue; }
      const e = text[i++];
      if (e === 'u') { out += String.fromCharCode(parseInt(text.slice(i, i + 4), 16)); i += 4; }
      else out += { b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' }[e] ?? e;
    }
  }
  function number() {
    const m = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i));
    i += m[0].length;
    return Number(m[0]);
  }

  const v = value();
  ws();
  if (i < text.length) fail(`unexpected '${text[i]}' at ${i}`);
  return v;
}

const quote = s => '"' + s.replace(/[\\"\n]/g, c => ({ '\\': '\\\\', '"': '\\"', '\n': '\\n' })[c]) + '"';

function show(v, indent = '') {
  const inner = indent + '  ';
  if (v instanceof Map) {
    if (!v.size) return '{}';
    return '{\n' + [...v].map(([k, x]) => inner + quote(k) + ': ' + show(x, inner)).join(',\n') + '\n' + indent + '}';
  }
  if (Array.isArray(v)) {
    if (!v.length) return '[]';
    return '[\n' + v.map(x => inner + show(x, inner)).join(',\n') + '\n' + indent + ']';
  }
  return typeof v === 'string' ? quote(v) : String(v);
}

function stats(v, s = { strings: 0, numbers: 0, sum: 0 }) {
  if (typeof v === 'string') s.strings++;
  else if (typeof v === 'number') { s.numbers++; s.sum += v; }
  else if (v instanceof Map || Array.isArray(v)) for (const x of v.values()) stats(x, s);
  return s;
}
const depth = v => (v instanceof Map || Array.isArray(v) ? 1 + Math.max(0, ...[...v.values()].map(depth)) : 0);

const doc = parse(DOC);
console.log(show(doc));
const s = stats(doc);
console.log(`strings: ${s.strings}, numbers: ${s.numbers}, sum: ${s.sum}, depth: ${depth(doc)}`);
for (const bad of ['[1, 2,]', '{"a" 1}', '"open']) {
  try { parse(bad); } catch (e) { if (!(e instanceof ParseError)) throw e; console.log(`error: ${e.message}`); }
}

The reference version: the parser is a closure over the text and a position i, with the five value functions nested inside it. An object becomes a Map rather than a plain object, because a Map keeps keys in insertion order whatever they look like (a plain object puts keys like "2" first). String.raw keeps the backslashes in the document as they are. Errors are a subclass of Error, so the catch can tell a parse error from a bug.

Python
import re

DOC = r'''{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}'''

NUMBER = re.compile(r"-?\d+(\.\d+)?([eE][+-]?\d+)?")
ESCAPES = {"b": "\b", "f": "\f", "n": "\n", "r": "\r", "t": "\t"}


class ParseError(Exception):
    pass


class Parser:
    def __init__(self, text):
        self.text, self.i = text, 0

    def peek(self):
        return self.text[self.i] if self.i < len(self.text) else ""

    def ws(self):
        while self.peek() in (" ", "\t", "\n", "\r"):
            self.i += 1

    def expect(self, c):
        self.ws()
        if self.peek() != c:
            raise ParseError(f"expected '{c}' at {self.i}")
        self.i += 1

    def parse(self):
        v = self.value()
        self.ws()
        if self.peek():
            raise ParseError(f"unexpected '{self.peek()}' at {self.i}")
        return v

    def value(self):
        self.ws()
        c = self.peek()
        if not c:
            raise ParseError("unexpected end of input")
        if c == "{":
            return self.object()
        if c == "[":
            return self.array()
        if c == '"':
            return self.string()
        if c == "-" or c.isdigit():
            return self.number()
        for word, v in (("true", True), ("false", False), ("null", None)):
            if self.text.startswith(word, self.i):
                self.i += len(word)
                return v
        raise ParseError(f"unexpected '{c}' at {self.i}")

    def object(self):
        pairs = {}
        self.i += 1
        self.ws()
        if self.peek() == "}":
            self.i += 1
            return pairs
        while True:
            self.ws()
            if self.peek() != '"':
                raise ParseError(f"expected string at {self.i}")
            key = self.string()
            self.expect(":")
            pairs[key] = self.value()
            self.ws()
            if self.peek() == ",":
                self.i += 1
            elif self.peek() == "}":
                self.i += 1
                return pairs
            else:
                raise ParseError(f"expected ',' or '}}' at {self.i}")

    def array(self):
        items = []
        self.i += 1
        self.ws()
        if self.peek() == "]":
            self.i += 1
            return items
        while True:
            items.append(self.value())
            self.ws()
            if self.peek() == ",":
                self.i += 1
            elif self.peek() == "]":
                self.i += 1
                return items
            else:
                raise ParseError(f"expected ',' or ']' at {self.i}")

    def string(self):
        out = []
        self.i += 1
        while True:
            c = self.peek()
            if not c:
                raise ParseError("unexpected end of input")
            self.i += 1
            if c == '"':
                return "".join(out)
            if c != "\\":
                out.append(c)
                continue
            e = self.peek()
            self.i += 1
            if e == "u":
                out.append(chr(int(self.text[self.i:self.i + 4], 16)))
                self.i += 4
            else:
                out.append(ESCAPES.get(e, e))

    def number(self):
        m = NUMBER.match(self.text, self.i)
        self.i = m.end()
        return float(m.group())


def quote(s):
    return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"'


def show(v, indent=""):
    inner = indent + "  "
    if isinstance(v, dict):
        if not v:
            return "{}"
        body = ",\n".join(f"{inner}{quote(k)}: {show(x, inner)}" for k, x in v.items())
        return "{\n" + body + "\n" + indent + "}"
    if isinstance(v, list):
        if not v:
            return "[]"
        return "[\n" + ",\n".join(inner + show(x, inner) for x in v) + "\n" + indent + "]"
    if isinstance(v, str):
        return quote(v)
    if v is None:
        return "null"
    if isinstance(v, bool):
        return "true" if v else "false"
    return str(int(v)) if v.is_integer() else repr(v)


def walk(v):
    yield v
    children = v.values() if isinstance(v, dict) else v if isinstance(v, list) else ()
    for x in children:
        yield from walk(x)


def depth(v):
    if isinstance(v, (dict, list)):
        return 1 + max((depth(x) for x in (v.values() if isinstance(v, dict) else v)), default=0)
    return 0


doc = Parser(DOC).parse()
print(show(doc))
values = list(walk(doc))
strings = [v for v in values if isinstance(v, str)]
numbers = [v for v in values if isinstance(v, float)]
print(f"strings: {len(strings)}, numbers: {len(numbers)}, sum: {show(sum(numbers))}, depth: {depth(doc)}")
for bad in ["[1, 2,]", '{"a" 1}', '"open']:
    try:
        Parser(bad).parse()
    except ParseError as e:
        print(f"error: {e}")

A class holding the text and position, and Python’s own types for the values: dict (which has kept insertion order since 3.7), list, str, float, True, False, None. A generator, walk, yields every value in the tree so the counts are list comprehensions. One trap: in Python True is an int, so counting "numbers" by isinstance(v, (int, float)) would count the boolean too — this parser makes every number a float and checks for that.

Ruby
require "strscan"

DOC = <<~'JSON'
  {
    "name": "Ada",
    "langs": ["C", "OCaml", "Prolog"],
    "age": 36, "ratio": -0.25, "big": 1.5e3,
    "active": true, "boss": null,
    "note": "say \"hi\"\n\u00e9t\u00e9",
    "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
  }
JSON

class ParseError < StandardError; end

class Parser
  ESCAPES = { "b" => "\b", "f" => "\f", "n" => "\n", "r" => "\r", "t" => "\t" }.freeze

  def initialize(text) = @s = StringScanner.new(text)

  def parse
    v = value
    ws
    raise ParseError, "unexpected '#{@s.peek(1)}' at #{@s.pos}" unless @s.eos?
    v
  end

  private

  def ws = @s.skip(/[ \t\n\r]*/)

  def expect(c)
    ws
    raise ParseError, "expected '#{c}' at #{@s.pos}" unless @s.skip(c)
  end

  def value
    ws
    raise ParseError, "unexpected end of input" if @s.eos?
    case @s.peek(1)
    when "{" then object
    when "[" then array
    when '"' then string
    when "-", /\d/ then @s.scan(/-?\d+(\.\d+)?([eE][+-]?\d+)?/).to_f
    else
      return true if @s.skip("true")
      return false if @s.skip("false")
      return nil if @s.skip("null")
      raise ParseError, "unexpected '#{@s.peek(1)}' at #{@s.pos}"
    end
  end

  def object
    pairs = {}
    @s.getch
    ws
    return pairs if @s.skip("}")
    loop do
      ws
      raise ParseError, "expected string at #{@s.pos}" unless @s.peek(1) == '"'
      key = string
      expect(":")
      pairs[key] = value
      ws
      next if @s.skip(",")
      return pairs if @s.skip("}")
      raise ParseError, "expected ',' or '}' at #{@s.pos}"
    end
  end

  def array
    items = []
    @s.getch
    ws
    return items if @s.skip("]")
    loop do
      items << value
      ws
      next if @s.skip(",")
      return items if @s.skip("]")
      raise ParseError, "expected ',' or ']' at #{@s.pos}"
    end
  end

  def string
    out = +""
    @s.getch
    loop do
      c = @s.getch or raise ParseError, "unexpected end of input"
      return out if c == '"'
      if c != "\\"
        out << c
      elsif (e = @s.getch) == "u"
        out << @s.peek(4).to_i(16).chr(Encoding::UTF_8)
        @s.pos += 4
      else
        out << ESCAPES.fetch(e, e)
      end
    end
  end
end

def quote(s) = '"' + s.gsub(/[\\"\n]/, "\\" => "\\\\", '"' => '\\"', "\n" => "\\n") + '"'

def show(v, indent = "")
  inner = indent + "  "
  case v
  in Hash if v.empty? then "{}"
  in Hash then "{\n" + v.map { |k, x| "#{inner}#{quote(k)}: #{show(x, inner)}" }.join(",\n") + "\n#{indent}}"
  in Array if v.empty? then "[]"
  in Array then "[\n" + v.map { |x| inner + show(x, inner) }.join(",\n") + "\n#{indent}]"
  in String then quote(v)
  in nil then "null"
  in Float if v == v.floor then v.to_i.to_s
  else v.to_s
  end
end

def leaves(v) = case v
                when Hash then v.values.flat_map { leaves(_1) }
                when Array then v.flat_map { leaves(_1) }
                else [v]
                end

def depth(v) = case v
               when Hash, Array then 1 + ((v.is_a?(Hash) ? v.values : v).map { depth(_1) }.max || 0)
               else 0
               end

doc = Parser.new(DOC).parse
puts show(doc)
numbers = leaves(doc).grep(Float)
puts "strings: #{leaves(doc).grep(String).size}, numbers: #{numbers.size}, sum: #{show(numbers.sum)}, depth: #{depth(doc)}"
['[1, 2,]', '{"a" 1}', '"open'].each do |bad|
  Parser.new(bad).parse
rescue ParseError => e
  puts "error: #{e.message}"
end
nil

StringScanner, from the standard library, is Ruby’s tool for exactly this: it keeps the position, and skip, scan and peek take strings or regular expressions. The pretty-printer uses pattern matching with guards (in Hash if v.empty?), and grep(Float) picks values by class because === on a class tests membership. rescue works on a do block without a begin.

Lua
local DOC = [[{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}]]

-- Lua tables cannot hold nil, and do not remember key order, so JSON null is a
-- unique sentinel and objects keep their keys in a list.
local NULL = setmetatable({}, { __tostring = function() return "null" end })
local ESCAPES = { b = "\b", f = "\f", n = "\n", r = "\r", t = "\t" }

local function parse(text)
  local i = 1 -- Lua strings start at 1; errors report i - 1 to match the other languages
  local function fail(msg) error({ msg = msg }, 0) end
  local function peek() return text:sub(i, i) end
  local function ws() i = text:find("[^ \t\n\r]", i) or #text + 1 end
  local function expect(c)
    ws()
    if peek() ~= c then fail(("expected '%s' at %d"):format(c, i - 1)) end
    i = i + 1
  end

  local value

  local function string_()
    local out = {}
    i = i + 1
    while true do
      local c = peek()
      if c == "" then fail("unexpected end of input") end
      i = i + 1
      if c == '"' then return table.concat(out) end
      if c ~= "\\" then
        out[#out + 1] = c
      else
        local e = peek()
        i = i + 1
        if e == "u" then
          out[#out + 1] = utf8.char(tonumber(text:sub(i, i + 3), 16))
          i = i + 4
        else
          out[#out + 1] = ESCAPES[e] or e
        end
      end
    end
  end

  local function object()
    local obj = { kind = "object", keys = {}, vals = {} }
    i = i + 1
    ws()
    if peek() == "}" then i = i + 1; return obj end
    while true do
      ws()
      if peek() ~= '"' then fail(("expected string at %d"):format(i - 1)) end
      local key = string_()
      expect(":")
      obj.keys[#obj.keys + 1] = key
      obj.vals[#obj.vals + 1] = value()
      ws()
      local c = peek()
      i = i + 1
      if c == "}" then return obj end
      if c ~= "," then fail(("expected ',' or '}' at %d"):format(i - 2)) end
    end
  end

  local function array()
    local arr = { kind = "array", vals = {} }
    i = i + 1
    ws()
    if peek() == "]" then i = i + 1; return arr end
    while true do
      arr.vals[#arr.vals + 1] = value()
      ws()
      local c = peek()
      i = i + 1
      if c == "]" then return arr end
      if c ~= "," then fail(("expected ',' or ']' at %d"):format(i - 2)) end
    end
  end

  function value()
    ws()
    local c = peek()
    if c == "" then fail("unexpected end of input") end
    if c == "{" then return object() end
    if c == "[" then return array() end
    if c == '"' then return string_() end
    local num = text:match("^-?%d+%.?%d*[eE]?[-+]?%d*", i)
    if num then i = i + #num; return tonumber(num) + 0.0 end
    for word, v in pairs({ ["true"] = true, ["false"] = false, null = NULL }) do
      if text:sub(i, i + #word - 1) == word then i = i + #word; return v end
    end
    fail(("unexpected '%s' at %d"):format(c, i - 1))
  end

  local v = value()
  ws()
  if i <= #text then fail(("unexpected '%s' at %d"):format(peek(), i - 1)) end
  return v
end

local function quote(s)
  return '"' .. s:gsub('[\\"\n]', { ["\\"] = "\\\\", ['"'] = '\\"', ["\n"] = "\\n" }) .. '"'
end

local function number(n)
  return n == math.floor(n) and ("%d"):format(n) or ("%.17g"):format(n)
end

local function show(v, indent)
  indent = indent or ""
  local inner = indent .. "  "
  if type(v) == "table" and v.kind then
    if #v.vals == 0 then return v.kind == "object" and "{}" or "[]" end
    local parts = {}
    for k, x in ipairs(v.vals) do
      parts[k] = inner .. (v.keys and quote(v.keys[k]) .. ": " or "") .. show(x, inner)
    end
    local open, close = "[", "]"
    if v.kind == "object" then open, close = "{", "}" end
    return open .. "\n" .. table.concat(parts, ",\n") .. "\n" .. indent .. close
  elseif type(v) == "string" then return quote(v)
  elseif type(v) == "number" then return number(v)
  else return tostring(v)
  end
end

local function walk(v, s)
  if type(v) == "string" then s.strings = s.strings + 1
  elseif type(v) == "number" then s.numbers, s.sum = s.numbers + 1, s.sum + v
  elseif type(v) == "table" and v.kind then
    for _, x in ipairs(v.vals) do walk(x, s) end
  end
  return s
end

local function depth(v)
  if type(v) ~= "table" or not v.kind then return 0 end
  local d = 0
  for _, x in ipairs(v.vals) do d = math.max(d, depth(x)) end
  return 1 + d
end

local doc = parse(DOC)
print(show(doc))
local s = walk(doc, { strings = 0, numbers = 0, sum = 0.0 })
print(("strings: %d, numbers: %d, sum: %s, depth: %d"):format(s.strings, s.numbers, number(s.sum), depth(doc)))
for _, bad in ipairs({ "[1, 2,]", '{"a" 1}', '"open' }) do
  local ok, err = pcall(parse, bad)
  if not ok then print("error: " .. err.msg) end
end

Lua has one data structure, so an object is a table holding a list of keys and a list of values, and JSON’s null has to be a unique sentinel table because a Lua table cannot store nil. Errors are any value passed to error and caught with pcall — here a table, so a real bug’s message (a string) would not be mistaken for a parse error. [[…]] long strings need no escaping, and utf8.char does the UTF-8 encoding.

PHP
<?php
const DOC = <<<'JSON'
{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}
JSON;

class JsonError extends Exception {}

final class Parser {
    private int $i = 0;
    private const ESCAPES = ['b' => "\x08", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t"];

    public function __construct(private string $text) {}

    public function parse(): mixed {
        $v = $this->value();
        $this->ws();
        if ($this->i < strlen($this->text)) $this->fail("unexpected '{$this->text[$this->i]}' at {$this->i}");
        return $v;
    }

    private function fail(string $msg): never { throw new JsonError($msg); }
    private function peek(): string { return $this->text[$this->i] ?? ''; }
    private function ws(): void { $this->i += strspn($this->text, " \t\n\r", $this->i); }

    private function expect(string $c): void {
        $this->ws();
        if ($this->peek() !== $c) $this->fail("expected '$c' at {$this->i}");
        $this->i++;
    }

    private function value(): mixed {
        $this->ws();
        $c = $this->peek();
        if ($c === '') $this->fail('unexpected end of input');
        if ($c === '{') return $this->object();
        if ($c === '[') return $this->array();
        if ($c === '"') return $this->string();
        if (preg_match('/-?\d+(\.\d+)?([eE][+-]?\d+)?/A', $this->text, $m, 0, $this->i)) {
            $this->i += strlen($m[0]);
            return (float) $m[0];
        }
        foreach (['true' => true, 'false' => false, 'null' => null] as $word => $v) {
            if (substr_compare($this->text, $word, $this->i, strlen($word)) === 0) {
                $this->i += strlen($word);
                return $v;
            }
        }
        $this->fail("unexpected '$c' at {$this->i}");
    }

    private function object(): stdClass {
        $obj = new stdClass();
        $this->i++;
        $this->ws();
        if ($this->peek() === '}') { $this->i++; return $obj; }
        while (true) {
            $this->ws();
            if ($this->peek() !== '"') $this->fail("expected string at {$this->i}");
            $key = $this->string();
            $this->expect(':');
            $obj->$key = $this->value();
            $this->ws();
            $c = $this->peek();
            if ($c === '}') { $this->i++; return $obj; }
            if ($c !== ',') $this->fail("expected ',' or '}' at {$this->i}");
            $this->i++;
        }
    }

    private function array(): array {
        $items = [];
        $this->i++;
        $this->ws();
        if ($this->peek() === ']') { $this->i++; return $items; }
        while (true) {
            $items[] = $this->value();
            $this->ws();
            $c = $this->peek();
            if ($c === ']') { $this->i++; return $items; }
            if ($c !== ',') $this->fail("expected ',' or ']' at {$this->i}");
            $this->i++;
        }
    }

    private function string(): string {
        $out = '';
        $this->i++;
        while (true) {
            $c = $this->peek();
            if ($c === '') $this->fail('unexpected end of input');
            $this->i++;
            if ($c === '"') return $out;
            if ($c !== '\\') { $out .= $c; continue; }
            $e = $this->peek();
            $this->i++;
            if ($e === 'u') {
                $out .= utf8(hexdec(substr($this->text, $this->i, 4)));
                $this->i += 4;
            } else {
                $out .= self::ESCAPES[$e] ?? $e;
            }
        }
    }
}

function utf8(int $cp): string {
    if ($cp < 0x80) return chr($cp);
    if ($cp < 0x800) return chr(0xC0 | $cp >> 6) . chr(0x80 | $cp & 0x3F);
    return chr(0xE0 | $cp >> 12) . chr(0x80 | $cp >> 6 & 0x3F) . chr(0x80 | $cp & 0x3F);
}

function quote(string $s): string {
    return '"' . strtr($s, ['\\' => '\\\\', '"' => '\\"', "\n" => '\\n']) . '"';
}

function show(mixed $v, string $indent = ''): string {
    $inner = "$indent  ";
    return match (true) {
        $v instanceof stdClass && !get_object_vars($v) => '{}',
        $v instanceof stdClass => "{\n" . implode(",\n", array_map(
            fn($k, $x) => $inner . quote((string) $k) . ': ' . show($x, $inner),
            array_keys(get_object_vars($v)), get_object_vars($v))) . "\n$indent}",
        $v === [] => '[]',
        is_array($v) => "[\n" . implode(",\n", array_map(fn($x) => $inner . show($x, $inner), $v)) . "\n$indent]",
        is_string($v) => quote($v),
        $v === null => 'null',
        is_bool($v) => $v ? 'true' : 'false',
        default => (string) $v,
    };
}

function leaves(mixed $v): array {
    if ($v instanceof stdClass) $v = get_object_vars($v);
    return is_array($v) ? array_merge([], ...array_map('leaves', array_values($v))) : [$v];
}

function depth(mixed $v): int {
    if ($v instanceof stdClass) $v = get_object_vars($v);
    return is_array($v) ? 1 + max([0, ...array_map('depth', $v)]) : 0;
}

$doc = (new Parser(DOC))->parse();
echo show($doc), "\n";
$numbers = array_filter(leaves($doc), 'is_float');
printf("strings: %d, numbers: %d, sum: %s, depth: %d\n",
    count(array_filter(leaves($doc), 'is_string')), count($numbers), show(array_sum($numbers)), depth($doc));
foreach (['[1, 2,]', '{"a" 1}', '"open'] as $bad) {
    try {
        (new Parser($bad))->parse();
    } catch (JsonError $e) {
        echo "error: {$e->getMessage()}\n";
    }
}

A PHP array is an ordered map and a list, which is handy until an empty one has to be printed: is [] an empty array or an empty object? So objects are stdClass instances (what PHP’s own json_decode returns by default) and arrays are arrays. Strings are bytes, so the utf8 function encodes \u escapes by hand; strspn skips whitespace in one call, and a regular expression with the A (anchored) flag matches a number exactly at the current offset. PHP 8’s never return type says that fail does not come back. (The exception is JsonError because PHP already has a built-in ParseError, for its own syntax errors.)

Perl
use strict;
use warnings;
binmode STDOUT, ':utf8';

my $DOC = <<'JSON';
{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}
JSON

# Values are tagged: [str => 'x'], [num => 1], [arr => [...]], [obj => [[key, value], ...]],
# [true], [false], [null] — a Perl scalar does not know whether it is a number or a string.
my %ESCAPES = (b => "\b", f => "\f", n => "\n", r => "\r", t => "\t");

sub parse {
    local $_ = shift;
    my $v = value();
    /\G\s*/gc;
    die sprintf("unexpected '%s' at %d\n", substr($_, pos), pos) if pos() < length;
    return $v;
}

sub value {
    /\G\s*/gc;
    die "unexpected end of input\n" if pos() >= length;
    return object() if /\G\{/gc;
    return array()  if /\G\[/gc;
    return [str => string()] if /\G"/gc;
    return [num => $1 + 0] if /\G(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/gc;
    return [$1] if /\G(true|false|null)/gc;
    die sprintf("unexpected '%s' at %d\n", substr($_, pos, 1), pos);
}

sub object {
    my @pairs;
    return [obj => \@pairs] if /\G\s*\}/gc;
    while (1) {
        /\G\s*/gc;
        die sprintf("expected string at %d\n", pos) unless /\G"/gc;
        my $key = string();
        /\G\s*/gc;
        die sprintf("expected ':' at %d\n", pos) unless /\G:/gc;
        push @pairs, [$key, value()];
        /\G\s*/gc;
        next if /\G,/gc;
        return [obj => \@pairs] if /\G\}/gc;
        die sprintf("expected ',' or '}' at %d\n", pos);
    }
}

sub array {
    my @items;
    return [arr => \@items] if /\G\s*\]/gc;
    while (1) {
        push @items, value();
        /\G\s*/gc;
        next if /\G,/gc;
        return [arr => \@items] if /\G\]/gc;
        die sprintf("expected ',' or ']' at %d\n", pos);
    }
}

sub string {
    my $out = '';
    while (1) {
        if    (/\G([^"\\]+)/gc)          { $out .= $1 }
        elsif (/\G"/gc)                  { return $out }
        elsif (/\G\\u([0-9a-fA-F]{4})/gc) { $out .= chr hex $1 }
        elsif (/\G\\(.)/gcs)             { $out .= $ESCAPES{$1} // $1 }
        else                             { die "unexpected end of input\n" }
    }
}

sub quote {
    my $s = shift;
    $s =~ s/([\\"])/\\$1/g;
    $s =~ s/\n/\\n/g;
    return qq{"$s"};
}

sub show {
    my ($v, $indent) = (@_, '');
    my ($type, $x) = @$v;
    my $inner = "$indent  ";
    return quote($x) if $type eq 'str';
    return $x if $type eq 'num';
    if ($type eq 'arr') {
        return '[]' unless @$x;
        return "[\n" . join(",\n", map { $inner . show($_, $inner) } @$x) . "\n$indent]";
    }
    if ($type eq 'obj') {
        return '{}' unless @$x;
        return "{\n" . join(",\n", map { $inner . quote($_->[0]) . ': ' . show($_->[1], $inner) } @$x) . "\n$indent}";
    }
    return $type;
}

sub children {
    my ($type, $x) = @{ $_[0] };
    return $type eq 'arr' ? @$x : $type eq 'obj' ? map { $_->[1] } @$x : ();
}

sub leaves { my @c = children($_[0]); @c ? map { leaves($_) } @c : $_[0] }

sub depth {
    my $v = shift;
    return 0 unless $v->[0] eq 'arr' || $v->[0] eq 'obj';
    my $d = 0;
    for (children($v)) { my $c = depth($_); $d = $c if $c > $d }
    return 1 + $d;
}

my $doc = parse($DOC);
print show($doc), "\n";
my @leaves = leaves($doc);
my @numbers = map { $_->[1] } grep { $_->[0] eq 'num' } @leaves;
my $sum = 0;
$sum += $_ for @numbers;
printf "strings: %d, numbers: %d, sum: %s, depth: %d\n",
    scalar(grep { $_->[0] eq 'str' } @leaves), scalar @numbers, $sum, depth($doc);
for my $bad ('[1, 2,]', '{"a" 1}', '"open') {
    eval { parse($bad); 1 } or print "error: $@";
}

The most Perl thing here is the tokenizer: the text is in $_, and each /\G…/gc match starts exactly where the last one stopped (\G) and, if it fails, leaves the position alone (c) — so the parser is a chain of "does the next bit look like this?". Perl scalars do not remember whether they hold a number or a string, so values are tagged by hand, and errors are die with a message ending in a newline (which stops Perl appending "at line 23") caught by eval.

Clojure
(def doc-text "{
  \"name\": \"Ada\",
  \"langs\": [\"C\", \"OCaml\", \"Prolog\"],
  \"age\": 36, \"ratio\": -0.25, \"big\": 1.5e3,
  \"active\": true, \"boss\": null,
  \"note\": \"say \\\"hi\\\"\\n\\u00e9t\\u00e9\",
  \"nested\": {\"a\": [1, 2, {\"b\": []}], \"c\": {}, \"path\": \"C:\\\\tmp\"}
}")

;; Each parse function takes the text and a position and returns [value next-position].
(defn fail [msg] (throw (ex-info msg {:json true})))
(defn skip-ws [s i] (if (and (< i (count s)) (#{\space \tab \newline \return} (nth s i))) (recur s (inc i)) i))
(defn at [s i] (when (< i (count s)) (nth s i)))

(declare parse-value)

(defn parse-string [s i]
  (loop [i (inc i) out []]
    (let [c (at s i)]
      (cond
        (nil? c) (fail "unexpected end of input")
        (= c \") [(apply str out) (inc i)]
        (not= c \\) (recur (inc i) (conj out c))
        (= (at s (inc i)) \u) (recur (+ i 6) (conj out (char (js/parseInt (subs s (+ i 2) (+ i 6)) 16))))
        :else (let [e (at s (inc i))]
                (recur (+ i 2) (conj out (get {\b "\b" \f "\f" \n "\n" \r "\r" \t "\t"} e e))))))))

(defn parse-seq
  "The shared shape of arrays and objects: items separated by commas until `close`."
  [s i close item]
  (let [i (skip-ws s (inc i))]
    (if (= (at s i) close)
      [[] (inc i)]
      (loop [i i items []]
        (let [[x i] (item s i)
              i (skip-ws s i)
              items (conj items x)]
          (condp = (at s i)
            \, (recur (inc i) items)
            close [items (inc i)]
            (fail (str "expected ',' or '" close "' at " i))))))))

(defn parse-pair [s i]
  (let [i (skip-ws s i)]
    (when-not (= (at s i) \") (fail (str "expected string at " i)))
    (let [[k i] (parse-string s i)
          i (skip-ws s i)]
      (when-not (= (at s i) \:) (fail (str "expected ':' at " i)))
      (let [[v i] (parse-value s (inc i))] [[k v] i]))))

(defn parse-value [s i]
  (let [i (skip-ws s i)
        c (at s i)
        rest-text (subs s (min i (count s)))]
    (cond
      (nil? c) (fail "unexpected end of input")
      (= c \{) (let [[pairs i] (parse-seq s i \} parse-pair)] [(apply array-map (apply concat pairs)) i])
      (= c \[) (parse-seq s i \] parse-value)
      (= c \") (parse-string s i)
      :else
      (if-let [n (re-find #"^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?" rest-text)]
        [(parse-double n) (+ i (count n))]
        (if-let [[word v] (some (fn [[w v]] (when (clojure.string/starts-with? rest-text w) [w v]))
                                [["true" true] ["false" false] ["null" nil]])]
          [v (+ i (count word))]
          (fail (str "unexpected '" c "' at " i)))))))

(defn parse [s]
  (let [[v i] (parse-value s 0)
        i (skip-ws s i)]
    (when (< i (count s)) (fail (str "unexpected '" (at s i) "' at " i)))
    v))

(defn quote-str [s] (str "\"" (clojure.string/escape s {\\ "\\\\" \" "\\\"" \newline "\\n"}) "\""))

(defn show
  ([v] (show v ""))
  ([v indent]
   (let [inner (str indent "  ")
         block (fn [open close lines]
                 (if (empty? lines) (str open close)
                     (str open "\n" (clojure.string/join ",\n" lines) "\n" indent close)))]
     (cond
       (map? v) (block "{" "}" (for [[k x] v] (str inner (quote-str k) ": " (show x inner))))
       (vector? v) (block "[" "]" (for [x v] (str inner (show x inner))))
       (string? v) (quote-str v)
       (nil? v) "null"
       :else (str v)))))

(defn depth [v]
  (if (coll? v) (inc (reduce max 0 (map depth (if (map? v) (vals v) v)))) 0))

(let [doc (parse doc-text)
      leaves (remove coll? (tree-seq coll? #(if (map? %) (vals %) %) doc))
      numbers (filter number? leaves)]
  (println (show doc))
  (println (str "strings: " (count (filter string? leaves)) ", numbers: " (count numbers)
                ", sum: " (reduce + numbers) ", depth: " (depth doc)))
  (doseq [bad ["[1, 2,]" "{\"a\" 1}" "\"open"]]
    (try (parse bad)
         (catch :default e
           (if (:json (ex-data e)) (println (str "error: " (ex-message e))) (throw e))))))

No mutable position at all: every parse function takes the text and an index and returns a pair, [value next-index], destructured as it comes back. Arrays and objects share one parse-seq, given a function to parse each item. Results are plain Clojure data — vectors, strings, numbers, nil — and objects are array-map`s, which keep insertion order (an ordinary Clojure map stops doing so beyond eight keys). `tree-seq walks the whole tree in one expression for the counts.

Static types

C
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static const char *DOC =
    "{\n"
    "  \"name\": \"Ada\",\n"
    "  \"langs\": [\"C\", \"OCaml\", \"Prolog\"],\n"
    "  \"age\": 36, \"ratio\": -0.25, \"big\": 1.5e3,\n"
    "  \"active\": true, \"boss\": null,\n"
    "  \"note\": \"say \\\"hi\\\"\\n\\u00e9t\\u00e9\",\n"
    "  \"nested\": {\"a\": [1, 2, {\"b\": []}], \"c\": {}, \"path\": \"C:\\\\tmp\"}\n"
    "}";

typedef enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ } Kind;

typedef struct Value {
    Kind kind;
    double num;            /* J_NUM; J_BOOL uses 0 or 1 */
    char *str;             /* J_STR */
    int n;                 /* J_ARR, J_OBJ: number of items */
    char **keys;           /* J_OBJ */
    struct Value **items;  /* J_ARR, J_OBJ */
} Value;

/* The parser's state. On an error, fail() records the first message and every function
   returns NULL (or 0) up the call chain. */
static const char *text;
static size_t pos;
static int failed;
static char message[80];

static void *fail(const char *fmt, ...) {
    if (!failed) {
        va_list ap;
        va_start(ap, fmt);
        vsnprintf(message, sizeof message, fmt, ap);
        va_end(ap);
        failed = 1;
    }
    return NULL;
}

static Value *new_value(Kind kind) {
    Value *v = calloc(1, sizeof *v);
    v->kind = kind;
    return v;
}

static void ws(void) {
    while (text[pos] == ' ' || text[pos] == '\t' || text[pos] == '\n' || text[pos] == '\r') pos++;
}

static int expect(char c) {
    ws();
    if (text[pos] != c) { fail("expected '%c' at %zu", c, pos); return 0; }
    pos++;
    return 1;
}

/* A growable string. */
typedef struct { char *s; size_t len, cap; } Buf;

static void put(Buf *b, char c) {
    if (b->len + 2 > b->cap) b->s = realloc(b->s, b->cap = b->cap * 2 + 16);
    b->s[b->len++] = c;
    b->s[b->len] = '\0';
}

static void put_utf8(Buf *b, unsigned cp) {
    if (cp < 0x80) {
        put(b, cp);
    } else if (cp < 0x800) {
        put(b, 0xC0 | cp >> 6);
        put(b, 0x80 | (cp & 0x3F));
    } else {
        put(b, 0xE0 | cp >> 12);
        put(b, 0x80 | (cp >> 6 & 0x3F));
        put(b, 0x80 | (cp & 0x3F));
    }
}

static Value *parse_value(void);

static char *parse_string(void) {
    Buf b = {0};
    put(&b, '\0');
    b.len = 0;
    pos++;
    for (;;) {
        char c = text[pos++];
        if (c == '\0') return fail("unexpected end of input");
        if (c == '"') return b.s;
        if (c != '\\') { put(&b, c); continue; }
        char e = text[pos++];
        switch (e) {
        case '\0': return fail("unexpected end of input");
        case 'b': put(&b, '\b'); break;
        case 'f': put(&b, '\f'); break;
        case 'n': put(&b, '\n'); break;
        case 'r': put(&b, '\r'); break;
        case 't': put(&b, '\t'); break;
        case 'u': {
            char hex[5] = {0};
            strncpy(hex, text + pos, 4);
            put_utf8(&b, strtoul(hex, NULL, 16));
            pos += strlen(hex);
            break;
        }
        default: put(&b, e);
        }
    }
}

static void add(Value *v, char *key, Value *item) {
    v->items = realloc(v->items, (v->n + 1) * sizeof *v->items);
    if (key) {
        v->keys = realloc(v->keys, (v->n + 1) * sizeof *v->keys);
        v->keys[v->n] = key;
    }
    v->items[v->n++] = item;
}

static Value *parse_object(void) {
    Value *v = new_value(J_OBJ);
    pos++;
    ws();
    if (text[pos] == '}') { pos++; return v; }
    for (;;) {
        ws();
        if (text[pos] != '"') return fail("expected string at %zu", pos);
        char *key = parse_string();
        if (!key || !expect(':')) return NULL;
        Value *item = parse_value();
        if (!item) return NULL;
        add(v, key, item);
        ws();
        if (text[pos] == ',') { pos++; continue; }
        if (text[pos] == '}') { pos++; return v; }
        return fail("expected ',' or '}' at %zu", pos);
    }
}

static Value *parse_array(void) {
    Value *v = new_value(J_ARR);
    pos++;
    ws();
    if (text[pos] == ']') { pos++; return v; }
    for (;;) {
        Value *item = parse_value();
        if (!item) return NULL;
        add(v, NULL, item);
        ws();
        if (text[pos] == ',') { pos++; continue; }
        if (text[pos] == ']') { pos++; return v; }
        return fail("expected ',' or ']' at %zu", pos);
    }
}

static Value *parse_value(void) {
    ws();
    char c = text[pos];
    if (c == '\0') return fail("unexpected end of input");
    if (c == '{') return parse_object();
    if (c == '[') return parse_array();
    if (c == '"') {
        char *s = parse_string();
        if (!s) return NULL;
        Value *v = new_value(J_STR);
        v->str = s;
        return v;
    }
    if (c == '-' || (c >= '0' && c <= '9')) {
        char *end;
        Value *v = new_value(J_NUM);
        v->num = strtod(text + pos, &end);
        pos = end - text;
        return v;
    }
    if (strncmp(text + pos, "true", 4) == 0) { pos += 4; Value *v = new_value(J_BOOL); v->num = 1; return v; }
    if (strncmp(text + pos, "false", 5) == 0) { pos += 5; return new_value(J_BOOL); }
    if (strncmp(text + pos, "null", 4) == 0) { pos += 4; return new_value(J_NULL); }
    return fail("unexpected '%c' at %zu", c, pos);
}

static Value *parse(const char *s) {
    text = s;
    pos = 0;
    failed = 0;
    Value *v = parse_value();
    if (!v) return NULL;
    ws();
    if (text[pos]) return fail("unexpected '%c' at %zu", text[pos], pos);
    return v;
}

static void print_number(double x) {
    if (x == floor(x) && fabs(x) < 1e15) printf("%.0f", x);
    else printf("%.17g", x);
}

static void print_string(const char *s) {
    putchar('"');
    for (; *s; s++) {
        if (*s == '"' || *s == '\\') printf("\\%c", *s);
        else if (*s == '\n') fputs("\\n", stdout);
        else putchar(*s);
    }
    putchar('"');
}

static void show(const Value *v, int indent) {
    switch (v->kind) {
    case J_NULL: fputs("null", stdout); break;
    case J_BOOL: fputs(v->num ? "true" : "false", stdout); break;
    case J_NUM: print_number(v->num); break;
    case J_STR: print_string(v->str); break;
    case J_ARR:
    case J_OBJ: {
        char open = v->kind == J_ARR ? '[' : '{', close = v->kind == J_ARR ? ']' : '}';
        if (v->n == 0) { printf("%c%c", open, close); break; }
        printf("%c\n", open);
        for (int k = 0; k < v->n; k++) {
            printf("%*s", indent + 2, "");
            if (v->kind == J_OBJ) { print_string(v->keys[k]); fputs(": ", stdout); }
            show(v->items[k], indent + 2);
            fputs(k + 1 < v->n ? ",\n" : "\n", stdout);
        }
        printf("%*s%c", indent, "", close);
    }
    }
}

static int strings, numbers;
static double sum;

static int walk(const Value *v) {  /* counts strings and numbers, returns the depth */
    if (v->kind == J_STR) strings++;
    if (v->kind == J_NUM) { numbers++; sum += v->num; }
    int deepest = 0;
    for (int k = 0; k < v->n; k++) {
        int d = walk(v->items[k]);
        if (d > deepest) deepest = d;
    }
    return v->kind == J_ARR || v->kind == J_OBJ ? 1 + deepest : 0;
}

static void try_parse(const char *s) {
    if (!parse(s)) printf("error: %s\n", message);
}

int main(void) {
    Value *doc = parse(DOC);
    if (!doc) { printf("error: %s\n", message); return 1; }
    show(doc, 0);
    putchar('\n');
    int depth = walk(doc);
    printf("strings: %d, numbers: %d, sum: ", strings, numbers);
    print_number(sum);
    printf(", depth: %d\n", depth);
    try_parse("[1, 2,]");
    try_parse("{\"a\" 1}");
    try_parse("\"open");
    return 0;
}

C has no "value of any type", so it is built: a tagged union in spirit (here a struct with a kind field and room for each kind), growable arrays by realloc, and a hand-written UTF-8 encoder. Errors are the C way: every function that can fail returns NULL, and every caller checks and passes it on. (The other C idiom, setjmp/longjmp back to the top, does not work here — WebAssembly needs its exception-handling extension for it.) The program never frees anything; a real one would allocate from an arena and free it in one go. strtod reads the numbers, and accepts a little more than JSON does.

Rust
const DOC: &str = r#"{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}"#;

enum Json {
    Null,
    Bool(bool),
    Num(f64),
    Str(String),
    Arr(Vec<Json>),
    Obj(Vec<(String, Json)>),
}

struct Parser<'a> {
    text: &'a [u8],
    pos: usize,
}

type Result<T> = std::result::Result<T, String>;

impl Parser<'_> {
    fn peek(&self) -> Option<u8> {
        self.text.get(self.pos).copied()
    }

    fn ws(&mut self) {
        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
            self.pos += 1;
        }
    }

    fn expect(&mut self, c: u8) -> Result<()> {
        self.ws();
        if self.peek() != Some(c) {
            return Err(format!("expected '{}' at {}", c as char, self.pos));
        }
        self.pos += 1;
        Ok(())
    }

    fn parse(mut self) -> Result<Json> {
        let v = self.value()?;
        self.ws();
        match self.peek() {
            Some(c) => Err(format!("unexpected '{}' at {}", c as char, self.pos)),
            None => Ok(v),
        }
    }

    fn value(&mut self) -> Result<Json> {
        self.ws();
        match self.peek() {
            None => Err("unexpected end of input".into()),
            Some(b'{') => self.object(),
            Some(b'[') => self.array(),
            Some(b'"') => Ok(Json::Str(self.string()?)),
            Some(b'-' | b'0'..=b'9') => Ok(self.number()),
            Some(c) => {
                for (word, v) in [("true", Json::Bool(true)), ("false", Json::Bool(false)), ("null", Json::Null)] {
                    if self.text[self.pos..].starts_with(word.as_bytes()) {
                        self.pos += word.len();
                        return Ok(v);
                    }
                }
                Err(format!("unexpected '{}' at {}", c as char, self.pos))
            }
        }
    }

    fn object(&mut self) -> Result<Json> {
        let mut pairs = Vec::new();
        self.pos += 1;
        self.ws();
        if self.peek() == Some(b'}') {
            self.pos += 1;
            return Ok(Json::Obj(pairs));
        }
        loop {
            self.ws();
            if self.peek() != Some(b'"') {
                return Err(format!("expected string at {}", self.pos));
            }
            let key = self.string()?;
            self.expect(b':')?;
            pairs.push((key, self.value()?));
            self.ws();
            match self.peek() {
                Some(b',') => self.pos += 1,
                Some(b'}') => {
                    self.pos += 1;
                    return Ok(Json::Obj(pairs));
                }
                _ => return Err(format!("expected ',' or '}}' at {}", self.pos)),
            }
        }
    }

    fn array(&mut self) -> Result<Json> {
        let mut items = Vec::new();
        self.pos += 1;
        self.ws();
        if self.peek() == Some(b']') {
            self.pos += 1;
            return Ok(Json::Arr(items));
        }
        loop {
            items.push(self.value()?);
            self.ws();
            match self.peek() {
                Some(b',') => self.pos += 1,
                Some(b']') => {
                    self.pos += 1;
                    return Ok(Json::Arr(items));
                }
                _ => return Err(format!("expected ',' or ']' at {}", self.pos)),
            }
        }
    }

    fn string(&mut self) -> Result<String> {
        let mut out = String::new();
        self.pos += 1;
        loop {
            let c = self.peek().ok_or("unexpected end of input")?;
            self.pos += 1;
            match c {
                b'"' => return Ok(out),
                b'\\' => {
                    let e = self.peek().ok_or("unexpected end of input")?;
                    self.pos += 1;
                    out.push(match e {
                        b'b' => '\u{8}',
                        b'f' => '\u{c}',
                        b'n' => '\n',
                        b'r' => '\r',
                        b't' => '\t',
                        b'u' => {
                            let hex = std::str::from_utf8(&self.text[self.pos..self.pos + 4]).unwrap();
                            self.pos += 4;
                            char::from_u32(u32::from_str_radix(hex, 16).unwrap()).unwrap_or('\u{fffd}')
                        }
                        other => other as char,
                    });
                }
                _ => {
                    // Copy a run of ordinary bytes at once: the text is UTF-8, so non-ASCII
                    // characters arrive as several bytes and must not be split.
                    let start = self.pos - 1;
                    while matches!(self.peek(), Some(b) if b != b'"' && b != b'\\') {
                        self.pos += 1;
                    }
                    out.push_str(std::str::from_utf8(&self.text[start..self.pos]).unwrap());
                }
            }
        }
    }

    fn number(&mut self) -> Json {
        let start = self.pos;
        while matches!(self.peek(), Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')) {
            self.pos += 1;
        }
        let s = std::str::from_utf8(&self.text[start..self.pos]).unwrap();
        Json::Num(s.parse().unwrap())
    }
}

fn parse(text: &str) -> Result<Json> {
    Parser { text: text.as_bytes(), pos: 0 }.parse()
}

fn quote(s: &str) -> String {
    format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n"))
}

fn show(v: &Json, indent: &str) -> String {
    let inner = format!("{indent}  ");
    let block = |open: &str, close: &str, lines: Vec<String>| {
        if lines.is_empty() {
            format!("{open}{close}")
        } else {
            format!("{open}\n{}\n{indent}{close}", lines.join(",\n"))
        }
    };
    match v {
        Json::Null => "null".into(),
        Json::Bool(b) => b.to_string(),
        Json::Num(n) => n.to_string(),
        Json::Str(s) => quote(s),
        Json::Arr(items) => block("[", "]", items.iter().map(|x| format!("{inner}{}", show(x, &inner))).collect()),
        Json::Obj(pairs) => block("{", "}", pairs.iter().map(|(k, x)| format!("{inner}{}: {}", quote(k), show(x, &inner))).collect()),
    }
}

fn children(v: &Json) -> Box<dyn Iterator<Item = &Json> + '_> {
    match v {
        Json::Arr(items) => Box::new(items.iter()),
        Json::Obj(pairs) => Box::new(pairs.iter().map(|(_, x)| x)),
        _ => Box::new(std::iter::empty()),
    }
}

fn depth(v: &Json) -> usize {
    match v {
        Json::Arr(_) | Json::Obj(_) => 1 + children(v).map(depth).max().unwrap_or(0),
        _ => 0,
    }
}

fn leaves<'a>(v: &'a Json, out: &mut Vec<&'a Json>) {
    match v {
        Json::Arr(_) | Json::Obj(_) => children(v).for_each(|x| leaves(x, out)),
        _ => out.push(v),
    }
}

fn main() {
    let doc = parse(DOC).expect("the document parses");
    println!("{}", show(&doc, ""));
    let mut all = Vec::new();
    leaves(&doc, &mut all);
    let strings = all.iter().filter(|v| matches!(v, Json::Str(_))).count();
    let numbers: Vec<f64> = all.iter().filter_map(|v| if let Json::Num(n) = v { Some(*n) } else { None }).collect();
    let sum: f64 = numbers.iter().sum();
    println!("strings: {strings}, numbers: {}, sum: {sum}, depth: {}", numbers.len(), depth(&doc));
    for bad in ["[1, 2,]", r#"{"a" 1}"#, "\"open"] {
        if let Err(e) = parse(bad) {
            println!("error: {e}");
        }
    }
}

A real sum type: enum Json has a variant per kind of value, and every match on it must handle them all. Errors are values — Result<Json, String> — and the ? operator returns early with the error, which reads almost like exceptions but is visible in every signature. The parser works on bytes and copies runs of ordinary characters whole, because slicing a UTF-8 string in the middle of a character is an error in Rust. And f64’s `Display prints 1500 and -0.25 with no help.

Go
package main

import (
	"fmt"
	"strconv"
	"strings"
)

const doc = `{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}`

// A JSON value is nil, bool, float64, string, []any or *Object.
type Object struct {
	Keys   []string
	Values []any
}

type parser struct {
	text string
	pos  int
}

func (p *parser) peek() byte {
	if p.pos < len(p.text) {
		return p.text[p.pos]
	}
	return 0
}

func (p *parser) ws() {
	for p.pos < len(p.text) && strings.IndexByte(" \t\n\r", p.text[p.pos]) >= 0 {
		p.pos++
	}
}

func (p *parser) expect(c byte) error {
	p.ws()
	if p.peek() != c {
		return fmt.Errorf("expected '%c' at %d", c, p.pos)
	}
	p.pos++
	return nil
}

func (p *parser) value() (any, error) {
	p.ws()
	if p.pos >= len(p.text) {
		return nil, fmt.Errorf("unexpected end of input")
	}
	switch c := p.peek(); {
	case c == '{':
		return p.object()
	case c == '[':
		return p.array()
	case c == '"':
		return p.string()
	case c == '-' || c >= '0' && c <= '9':
		start := p.pos
		for p.pos < len(p.text) && strings.IndexByte("+-.eE0123456789", p.text[p.pos]) >= 0 {
			p.pos++
		}
		return strconv.ParseFloat(p.text[start:p.pos], 64)
	}
	for word, v := range map[string]any{"true": true, "false": false, "null": nil} {
		if strings.HasPrefix(p.text[p.pos:], word) {
			p.pos += len(word)
			return v, nil
		}
	}
	return nil, fmt.Errorf("unexpected '%c' at %d", p.peek(), p.pos)
}

func (p *parser) object() (any, error) {
	obj := &Object{}
	p.pos++
	p.ws()
	if p.peek() == '}' {
		p.pos++
		return obj, nil
	}
	for {
		p.ws()
		if p.peek() != '"' {
			return nil, fmt.Errorf("expected string at %d", p.pos)
		}
		key, err := p.string()
		if err != nil {
			return nil, err
		}
		if err := p.expect(':'); err != nil {
			return nil, err
		}
		v, err := p.value()
		if err != nil {
			return nil, err
		}
		obj.Keys = append(obj.Keys, key.(string))
		obj.Values = append(obj.Values, v)
		p.ws()
		switch p.peek() {
		case ',':
			p.pos++
		case '}':
			p.pos++
			return obj, nil
		default:
			return nil, fmt.Errorf("expected ',' or '}' at %d", p.pos)
		}
	}
}

func (p *parser) array() (any, error) {
	items := []any{}
	p.pos++
	p.ws()
	if p.peek() == ']' {
		p.pos++
		return items, nil
	}
	for {
		v, err := p.value()
		if err != nil {
			return nil, err
		}
		items = append(items, v)
		p.ws()
		switch p.peek() {
		case ',':
			p.pos++
		case ']':
			p.pos++
			return items, nil
		default:
			return nil, fmt.Errorf("expected ',' or ']' at %d", p.pos)
		}
	}
}

func (p *parser) string() (any, error) {
	var out strings.Builder
	p.pos++
	for {
		if p.pos >= len(p.text) {
			return nil, fmt.Errorf("unexpected end of input")
		}
		c := p.text[p.pos]
		p.pos++
		switch {
		case c == '"':
			return out.String(), nil
		case c != '\\':
			out.WriteByte(c)
		case p.peek() == 'u':
			n, _ := strconv.ParseUint(p.text[p.pos+1:p.pos+5], 16, 32)
			out.WriteRune(rune(n))
			p.pos += 5
		default:
			e := p.peek()
			p.pos++
			if r, ok := map[byte]byte{'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}[e]; ok {
				e = r
			}
			out.WriteByte(e)
		}
	}
}

func parse(text string) (any, error) {
	p := &parser{text: text}
	v, err := p.value()
	if err != nil {
		return nil, err
	}
	p.ws()
	if p.pos < len(p.text) {
		return nil, fmt.Errorf("unexpected '%c' at %d", p.peek(), p.pos)
	}
	return v, nil
}

var quoter = strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", `\n`)

func show(v any, indent string) string {
	inner := indent + "  "
	block := func(open, close string, lines []string) string {
		if len(lines) == 0 {
			return open + close
		}
		return open + "\n" + strings.Join(lines, ",\n") + "\n" + indent + close
	}
	switch v := v.(type) {
	case nil:
		return "null"
	case bool:
		return strconv.FormatBool(v)
	case float64:
		return strconv.FormatFloat(v, 'f', -1, 64)
	case string:
		return `"` + quoter.Replace(v) + `"`
	case []any:
		lines := []string{}
		for _, x := range v {
			lines = append(lines, inner+show(x, inner))
		}
		return block("[", "]", lines)
	case *Object:
		lines := []string{}
		for k, key := range v.Keys {
			lines = append(lines, inner+show(key, inner)+": "+show(v.Values[k], inner))
		}
		return block("{", "}", lines)
	}
	panic("not a JSON value")
}

func children(v any) []any {
	switch v := v.(type) {
	case []any:
		return v
	case *Object:
		return v.Values
	}
	return nil
}

func depth(v any) int {
	switch v.(type) {
	case []any, *Object:
		d := 0
		for _, x := range children(v) {
			d = max(d, depth(x))
		}
		return 1 + d
	}
	return 0
}

func main() {
	v, err := parse(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(show(v, ""))
	strs, nums, sum := 0, 0, 0.0
	var walk func(any)
	walk = func(v any) {
		switch x := v.(type) {
		case string:
			strs++
		case float64:
			nums++
			sum += x
		}
		for _, c := range children(v) {
			walk(c)
		}
	}
	walk(v)
	fmt.Printf("strings: %d, numbers: %d, sum: %s, depth: %d\n", strs, nums, show(sum, ""), depth(v))
	for _, bad := range []string{"[1, 2,]", `{"a" 1}`, `"open`} {
		if _, err := parse(bad); err != nil {
			fmt.Println("error:", err)
		}
	}
}

Go’s any holds a value of any type, and a type switch (switch v := v.(type)) takes it apart again — the same representation the standard encoding/json package uses, except that a Go map has no order, so objects are a small struct of keys and values. Every function that can fail returns (value, error) and every caller checks: the famous if err != nil that makes up a good fifth of this program. strings.Builder.WriteRune encodes UTF-8.

Kotlin
const val DOC = """{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}"""

sealed interface Json {
    data object Null : Json
    data class Bool(val value: Boolean) : Json
    data class Num(val value: Double) : Json
    data class Str(val value: String) : Json
    data class Arr(val items: List<Json>) : Json
    data class Obj(val pairs: List<Pair<String, Json>>) : Json
}

class ParseError(message: String) : Exception(message)

class Parser(private val text: String) {
    private var i = 0

    private fun peek(): Char? = text.getOrNull(i)
    private fun fail(msg: String): Nothing = throw ParseError(msg)
    private fun ws() { while (peek()?.let { it in " \t\n\r" } == true) i++ }
    private fun expect(c: Char) { ws(); if (peek() != c) fail("expected '$c' at $i"); i++ }

    fun parse(): Json {
        val v = value()
        ws()
        if (i < text.length) fail("unexpected '${text[i]}' at $i")
        return v
    }

    private fun value(): Json {
        ws()
        val c = peek() ?: fail("unexpected end of input")
        return when {
            c == '{' -> obj()
            c == '[' -> arr()
            c == '"' -> Json.Str(string())
            c == '-' || c.isDigit() -> {
                val m = Regex("""-?\d+(\.\d+)?([eE][+-]?\d+)?""").matchAt(text, i)!!
                i = m.range.last + 1
                Json.Num(m.value.toDouble())
            }
            text.startsWith("true", i) -> { i += 4; Json.Bool(true) }
            text.startsWith("false", i) -> { i += 5; Json.Bool(false) }
            text.startsWith("null", i) -> { i += 4; Json.Null }
            else -> fail("unexpected '$c' at $i")
        }
    }

    private fun <T> items(close: Char, item: () -> T): List<T> {
        val out = mutableListOf<T>()
        i++
        ws()
        if (peek() == close) { i++; return out }
        while (true) {
            out += item()
            ws()
            when (peek()) {
                ',' -> i++
                close -> { i++; return out }
                else -> fail("expected ',' or '$close' at $i")
            }
        }
    }

    private fun obj() = Json.Obj(items('}') {
        ws()
        if (peek() != '"') fail("expected string at $i")
        val key = string()
        expect(':')
        key to value()
    })

    private fun arr() = Json.Arr(items(']') { value() })

    private fun string(): String = buildString {
        i++
        while (true) {
            val c = peek() ?: fail("unexpected end of input")
            i++
            when {
                c == '"' -> return@buildString
                c != '\\' -> append(c)
                peek() == 'u' -> { append(text.substring(i + 1, i + 5).toInt(16).toChar()); i += 5 }
                else -> {
                    val e = text[i++]
                    append(mapOf('b' to '\b', 'f' to '\u000c', 'n' to '\n', 'r' to '\r', 't' to '\t')[e] ?: e)
                }
            }
        }
    }
}

fun quote(s: String) = "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\""

fun num(d: Double) = if (d % 1.0 == 0.0 && Math.abs(d) < 1e15) d.toLong().toString() else d.toString()

fun show(v: Json, indent: String = ""): String {
    val inner = "$indent  "
    fun block(open: String, close: String, lines: List<String>) =
        if (lines.isEmpty()) open + close else "$open\n${lines.joinToString(",\n")}\n$indent$close"
    return when (v) {
        Json.Null -> "null"
        is Json.Bool -> v.value.toString()
        is Json.Num -> num(v.value)
        is Json.Str -> quote(v.value)
        is Json.Arr -> block("[", "]", v.items.map { inner + show(it, inner) })
        is Json.Obj -> block("{", "}", v.pairs.map { (k, x) -> "$inner${quote(k)}: ${show(x, inner)}" })
    }
}

fun children(v: Json): List<Json> = when (v) {
    is Json.Arr -> v.items
    is Json.Obj -> v.pairs.map { it.second }
    else -> emptyList()
}

fun leaves(v: Json): Sequence<Json> =
    if (v is Json.Arr || v is Json.Obj) children(v).asSequence().flatMap(::leaves) else sequenceOf(v)

fun depth(v: Json): Int = if (v is Json.Arr || v is Json.Obj) 1 + (children(v).maxOfOrNull(::depth) ?: 0) else 0

fun main() {
    val doc = Parser(DOC).parse()
    println(show(doc))
    val all = leaves(doc).toList()
    val numbers = all.filterIsInstance<Json.Num>().map { it.value }
    println("strings: ${all.count { it is Json.Str }}, numbers: ${numbers.size}, sum: ${num(numbers.sum())}, depth: ${depth(doc)}")
    for (bad in listOf("[1, 2,]", """{"a" 1}""", "\"open")) {
        try {
            Parser(bad).parse()
        } catch (e: ParseError) {
            println("error: ${e.message}")
        }
    }
}

A sealed interface is Kotlin’s sum type: the compiler knows every subclass, so the when in show needs no else. fail returns Nothing, which is what lets peek() ?: fail(…) type-check as a Char. Arrays and objects share items, a generic function taking a lambda for each item, and buildString makes the string parser an expression. Raw strings ("""…""") hold the document as it is.

OCaml
let doc = {|{
  "name": "Ada",
  "langs": ["C", "OCaml", "Prolog"],
  "age": 36, "ratio": -0.25, "big": 1.5e3,
  "active": true, "boss": null,
  "note": "say \"hi\"\n\u00e9t\u00e9",
  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\tmp"}
}|}

type json =
  | Null
  | Bool of bool
  | Num of float
  | Str of string
  | Arr of json list
  | Obj of (string * json) list

exception Parse_error of string

let fail fmt = Printf.ksprintf (fun s -> raise (Parse_error s)) fmt

let parse text =
  let len = String.length text in
  let pos = ref 0 in
  let peek () = if !pos < len then Some text.[!pos] else None in
  let rec ws () = match peek () with Some (' ' | '\t' | '\n' | '\r') -> incr pos; ws () | _ -> () in
  let expect c =
    ws ();
    if peek () <> Some c then fail "expected '%c' at %d" c !pos;
    incr pos
  in
  let looking_at word = !pos + String.length word <= len && String.sub text !pos (String.length word) = word in
  let string () =
    let buf = Buffer.create 16 in
    incr pos;
    let rec loop () =
      match peek () with
      | None -> fail "unexpected end of input"
      | Some '"' -> incr pos; Buffer.contents buf
      | Some '\\' ->
        incr pos;
        (match peek () with
         | Some 'u' ->
           let code = int_of_string ("0x" ^ String.sub text (!pos + 1) 4) in
           Buffer.add_utf_8_uchar buf (Uchar.of_int code);
           pos := !pos + 5
         | Some e ->
           Buffer.add_char buf
             (match e with 'b' -> '\b' | 'f' -> '\012' | 'n' -> '\n' | 'r' -> '\r' | 't' -> '\t' | c -> c);
           incr pos
         | None -> fail "unexpected end of input");
        loop ()
      | Some c -> Buffer.add_char buf c; incr pos; loop ()
    in
    loop ()
  in
  (* Items separated by commas until [close]: the shared shape of arrays and objects. *)
  let items close item =
    incr pos;
    ws ();
    if peek () = Some close then (incr pos; [])
    else
      let rec loop acc =
        let acc = item () :: acc in
        ws ();
        match peek () with
        | Some ',' -> incr pos; loop acc
        | Some c when c = close -> incr pos; List.rev acc
        | _ -> fail "expected ',' or '%c' at %d" close !pos
      in
      loop []
  in
  let rec value () =
    ws ();
    match peek () with
    | None -> fail "unexpected end of input"
    | Some '{' ->
      Obj (items '}' (fun () ->
          ws ();
          if peek () <> Some '"' then fail "expected string at %d" !pos;
          let key = string () in
          expect ':';
          (key, value ())))
    | Some '[' -> Arr (items ']' value)
    | Some '"' -> Str (string ())
    | Some ('-' | '0' .. '9') ->
      let start = !pos in
      while (match peek () with Some ('-' | '+' | '.' | 'e' | 'E' | '0' .. '9') -> true | _ -> false) do
        incr pos
      done;
      Num (float_of_string (String.sub text start (!pos - start)))
    | Some _ when looking_at "true" -> pos := !pos + 4; Bool true
    | Some _ when looking_at "false" -> pos := !pos + 5; Bool false
    | Some _ when looking_at "null" -> pos := !pos + 4; Null
    | Some c -> fail "unexpected '%c' at %d" c !pos
  in
  let v = value () in
  ws ();
  (match peek () with Some c -> fail "unexpected '%c' at %d" c !pos | None -> ());
  v

let quote s =
  let buf = Buffer.create (String.length s + 2) in
  Buffer.add_char buf '"';
  String.iter (function
      | '"' -> Buffer.add_string buf "\\\""
      | '\\' -> Buffer.add_string buf "\\\\"
      | '\n' -> Buffer.add_string buf "\\n"
      | c -> Buffer.add_char buf c) s;
  Buffer.add_char buf '"';
  Buffer.contents buf

let number n =
  if Float.is_integer n && Float.abs n < 1e15 then Printf.sprintf "%.0f" n else Printf.sprintf "%.17g" n

let rec show ?(indent = "") v =
  let inner = indent ^ "  " in
  let block opn cls = function
    | [] -> opn ^ cls
    | lines -> opn ^ "\n" ^ String.concat ",\n" lines ^ "\n" ^ indent ^ cls
  in
  match v with
  | Null -> "null"
  | Bool b -> string_of_bool b
  | Num n -> number n
  | Str s -> quote s
  | Arr items -> block "[" "]" (List.map (fun x -> inner ^ show ~indent:inner x) items)
  | Obj pairs -> block "{" "}" (List.map (fun (k, x) -> inner ^ quote k ^ ": " ^ show ~indent:inner x) pairs)

let children = function Arr items -> items | Obj pairs -> List.map snd pairs | _ -> []

let is_container = function Arr _ | Obj _ -> true | _ -> false

let rec leaves v = if is_container v then List.concat_map leaves (children v) else [ v ]

let rec depth v = if is_container v then 1 + List.fold_left (fun d x -> max d (depth x)) 0 (children v) else 0

let () =
  let d = parse doc in
  print_endline (show d);
  let all = leaves d in
  let numbers = List.filter_map (function Num n -> Some n | _ -> None) all in
  let strings = List.length (List.filter (function Str _ -> true | _ -> false) all) in
  Printf.printf "strings: %d, numbers: %d, sum: %s, depth: %d\n" strings (List.length numbers)
    (number (List.fold_left ( +. ) 0. numbers)) (depth d);
  List.iter (fun bad -> try ignore (parse bad) with Parse_error msg -> Printf.printf "error: %s\n" msg)
    [ "[1, 2,]"; {|{"a" 1}|}; "\"open" ]

The language ML was made to write compilers in, and it shows: the json type is six lines, pattern matching does all the dispatch (including on character ranges, '0' .. '9'), and the compiler warns about any case a match forgets. fail is built on ksprintf, so it takes a type-checked format string like printf and raises instead of printing. {|…|} quoted strings hold the document with no escaping.

Logic

Prolog
% Values: json(Pairs) for objects (Pairs = [Key-Value, ...]), lists for arrays, strings,
% numbers, and the atoms true, false and null. Parsing is a DCG over character codes.

doc(Doc) :- atomic_list_concat([
  '{',
  '  "name": "Ada",',
  '  "langs": ["C", "OCaml", "Prolog"],',
  '  "age": 36, "ratio": -0.25, "big": 1.5e3,',
  '  "active": true, "boss": null,',
  '  "note": "say \\"hi\\"\\n\\u00e9t\\u00e9",',
  '  "nested": {"a": [1, 2, {"b": []}], "c": {}, "path": "C:\\\\tmp"}',
  '}'], '\n', Doc).

parse(Text, Value) :-
    atom_codes(Text, Codes),
    length(Codes, Len),
    nb_setval(json_length, Len),
    phrase(json(Value), Codes).

json(V) --> value(V), ws, end_of_input.

end_of_input --> here(P), [C], !, { throw_at("unexpected '~c' at ~d", [C, P]) }.
end_of_input --> [].

% here(P): the position of the rest of the input, counted from the start.
here(P, S, S) :- nb_getval(json_length, Len), length(S, N), P is Len - N.

throw_at(Format, Args) :- format(string(M), Format, Args), throw(json_error(M)).

ws --> [C], { memberchk(C, ` \t\n\r`) }, !, ws.
ws --> [].

value(V) --> ws, ( peek(C) -> value(C, V) ; { throw(json_error("unexpected end of input")) } ).

peek(C, [C|T], [C|T]).

value(0'{, V) --> !, object(V).
value(0'[, V) --> !, array(V).
value(0'", S) --> !, string(S).
value(C, N) --> { C == 0'- ; code_type(C, digit) }, !, number(N).
value(_, true) --> "true", !.
value(_, false) --> "false", !.
value(_, null) --> "null", !.
value(C, _) --> here(P), { throw_at("unexpected '~c' at ~d", [C, P]) }.

object(json(Pairs)) --> "{", ws, ( "}" -> { Pairs = [] } ; members(Pairs) ).
members([K-V|Ps]) -->
    ws, key(K), ws, expect(0':), value(V), ws,
    (   "," -> members(Ps)
    ;   "}" -> { Ps = [] }
    ;   here(P), { throw_at("expected ',' or '}' at ~d", [P]) }
    ).

key(K) --> peek(0'"), !, string(K).
key(_) --> here(P), { throw_at("expected string at ~d", [P]) }.

expect(C) --> [C], !.
expect(C) --> here(P), { throw_at("expected '~c' at ~d", [C, P]) }.

array(Items) --> "[", ws, ( "]" -> { Items = [] } ; elements(Items) ).
elements([V|Vs]) -->
    value(V), ws,
    (   "," -> elements(Vs)
    ;   "]" -> { Vs = [] }
    ;   here(P), { throw_at("expected ',' or ']' at ~d", [P]) }
    ).

string(S) --> "\"", chars(Cs), { string_codes(S, Cs) }.
chars([]) --> "\"", !.
chars([C|Cs]) --> "\\", !, escape(C), chars(Cs).
chars([C|Cs]) --> [C], !, chars(Cs).
chars(_) --> { throw(json_error("unexpected end of input")) }.

escape(0'\b) --> "b", !.
escape(0'\f) --> "f", !.
escape(0'\n) --> "n", !.
escape(0'\r) --> "r", !.
escape(0'\t) --> "t", !.
escape(C) --> "u", !, [H1, H2, H3, H4], { number_codes(C, [0'0, 0'x, H1, H2, H3, H4]) }.
escape(C) --> [C].

number(N) --> num_codes(Cs), { number_codes(N0, Cs), N is float(N0) }.
num_codes([C|Cs]) --> [C], { memberchk(C, `+-.eE0123456789`) }, !, num_codes(Cs).
num_codes([]) --> [].

% Printing.
show(V) :- show(V, 0).
show(json([]), _) :- !, write('{}').
show(json(Pairs), I) :- !, block('{', '}', Pairs, I).
show([], _) :- !, write('[]').
show(Items, I) :- is_list(Items), !, block('[', ']', Items, I).
show(S, _) :- string(S), !, quote(S).
show(N, _) :- number(N), !, show_number(N).
show(Atom, _) :- write(Atom).

block(Open, Close, Items, I) :-
    I2 is I + 2,
    write(Open), nl,
    show_items(Items, I2),
    tab(I), write(Close).

show_items([X], I) :- !, tab(I), show_item(X, I), nl.
show_items([X|Xs], I) :- tab(I), show_item(X, I), write(','), nl, show_items(Xs, I).

show_item(K-V, I) :- string(K), !, quote(K), write(': '), show(V, I).
show_item(V, I) :- show(V, I).

show_number(N) :- N =:= truncate(N), abs(N) < 1.0e15, !, X is truncate(N), write(X).
show_number(N) :- write(N).

quote(S) :- string_codes(S, Cs), write('"'), forall(member(C, Cs), quote_code(C)), write('"').
quote_code(0'") :- !, write('\\"').
quote_code(0'\\) :- !, write('\\\\').
quote_code(0'\n) :- !, write('\\n').
quote_code(C) :- char_code(Ch, C), put_char(Ch).

% Counting.
leaves(json(Pairs), Ls) :- !, findall(V, member(_-V, Pairs), Vs), maplist(leaves, Vs, Lss), append(Lss, Ls).
leaves([], []) :- !.
leaves(Items, Ls) :- is_list(Items), !, maplist(leaves, Items, Lss), append(Lss, Ls).
leaves(V, [V]).

depth(json(Pairs), D) :- !, findall(X, (member(_-V, Pairs), depth(V, X)), Ds), max_list([0|Ds], M), D is M + 1.
depth(Items, D) :- is_list(Items), !, findall(X, (member(V, Items), depth(V, X)), Ds), max_list([0|Ds], M), D is M + 1.
depth(_, 0).

main :-
    doc(Text),
    parse(Text, Doc),
    show(Doc), nl,
    leaves(Doc, Leaves),
    include(string, Leaves, Strings), length(Strings, NS),
    include(number, Leaves, Numbers), length(Numbers, NN), sum_list(Numbers, Sum),
    depth(Doc, D),
    format("strings: ~d, numbers: ~d, sum: ", [NS, NN]), show_number(Sum), format(", depth: ~d~n", [D]),
    forall(member(Bad, ['[1, 2,]', '{"a" 1}', '"open']),
           catch(parse(Bad, _), json_error(M), format("error: ~s~n", [M]))).

?- main.

A definite clause grammar (DCG) is Prolog’s built-in notation for parsers: -→ rules read like the grammar and are translated into predicates that thread the remaining input through. Values need no type declaration — json(Pairs), a list, a string, a number or an atom — because Prolog terms are already a tree. Two things DCGs do not give you for free are positions and good errors: this one records the input’s length and works out the position from how much is left (here//1), and throws json_error(Message) where an ordinary DCG would quietly fail and backtrack.

What it shows

"Any JSON value" is… An error is…

JavaScript, Python, Ruby, Lua, PHP, Clojure

the language’s own values (with a sentinel for null in Lua)

an exception (Lua: error/pcall)

Perl

a hand-tagged [type, value] pair

die caught by eval

C

a struct with a kind tag

NULL, returned and checked at every call

Rust, Kotlin, OCaml

a sum type: enum, sealed interface, variant type

Result and ? (Rust); exceptions (Kotlin, OCaml)

Go

any, taken apart with a type switch

a returned error, checked at every call

Prolog

a term

a thrown term — a failing parse would otherwise just backtrack

The sum-type languages (Rust, Kotlin, OCaml) are the ones where the compiler checks the printer handles every kind of value; in the dynamic ones, forgetting null is found when a document contains one. The order of an object’s keys turned out to need thought in nearly every language: JavaScript’s Map, Python’s dict and Ruby’s Hash keep it, Clojure’s array-map keeps it on request, and Lua, Perl, Go and C needed a list of pairs. And the Unicode escape split them three ways: languages whose strings are characters (JavaScript, Python, Kotlin, Clojure) just make the character; languages with byte strings and a UTF-8 helper (Lua’s utf8.char, OCaml’s Buffer.add_utf_8_uchar, Go’s WriteRune, Rust’s char) call it; and C and PHP encode the bytes by hand.

All thirteen print the same 29 lines — scripts/check-rosetta.mjs in the site’s repository runs them in a headless browser and compares.