Skip to content

JSON Formatter — Rust source

Beautify, minify and validate JSON instantly. Paste, format, copy - all in your browser. Share a link to your exact input.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

// JSON formatter — parse, pretty-print (2-space indent), and minify.
//
// Language: Rust
// CosmoDev polyglot showcase port of the `json-formatter` tool.
// Ported from src/tools/JsonFormatter.tsx — display source, part of CosmoDev's
// polyglot tool pages.
//
// The Rust standard library ships no JSON engine, so — unlike the
// TypeScript/JavaScript/Go/PHP/Python ports, which lean on a stdlib JSON
// implementation — this file includes a small hand-written recursive-descent
// parser and serializer (std only, no serde and no external crates). Object
// members are stored in a Vec<(String, Json)>, which preserves source
// insertion order just like a JavaScript engine.
//
// Parity caveats: numbers are held as f64 (matching JS Number), so integers
// beyond 2^53 lose precision and Rust's f64 Display diverges from ECMAScript's
// Number::toString near the exponential-notation thresholds (e.g. 1e21 prints
// in full rather than as "1e+21"). For ordinary integers and decimals the
// output is byte-identical to JSON.stringify.

#![forbid(unsafe_code)]

use std::fmt::Write as _;

/// A parsed JSON value. Object members live in a Vec to preserve source order.
#[derive(Debug, Clone, PartialEq)]
pub enum Json {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<Json>),
    Object(Vec<(String, Json)>),
}

/// The format result. `error == None` means success or empty input.
#[derive(Debug, Clone, PartialEq)]
pub struct FormatResult {
    pub output: String,
    pub error: Option<String>,
}

/// Selects the output shape.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
    Beautify,
    Minify,
}

/// A parse failure: byte offset + message.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
    pub offset: usize,
    pub message: &'static str,
}

/// Parse and re-emit a JSON document the way the live island does. `indent` is
/// the number of spaces per nesting level for Mode::Beautify (pass 2 to match
/// the UI). Empty/whitespace-only input yields empty output, no error.
pub fn format_json(input: &str, mode: Mode, indent: usize) -> FormatResult {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return FormatResult { output: String::new(), error: None };
    }
    match Parser::new(trimmed).parse_document() {
        Ok(value) => {
            let pad = " ".repeat(indent);
            let mut out = String::new();
            match mode {
                Mode::Minify => value.write_compact(&mut out),
                Mode::Beautify => value.write_pretty(&mut out, 0, &pad),
            }
            FormatResult { output: out, error: None }
        }
        Err(e) => FormatResult {
            output: String::new(),
            error: Some(e.message.to_string()),
        },
    }
}

impl Json {
    fn write_compact(&self, out: &mut String) {
        match self {
            Json::Null => out.push_str("null"),
            Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
            Json::Number(n) => {
                let _ = write!(out, "{n}");
            }
            Json::Str(s) => write_json_string(out, s),
            Json::Array(items) => {
                out.push('[');
                for (i, v) in items.iter().enumerate() {
                    if i > 0 {
                        out.push(',');
                    }
                    v.write_compact(out);
                }
                out.push(']');
            }
            Json::Object(members) => {
                out.push('{');
                for (i, (k, v)) in members.iter().enumerate() {
                    if i > 0 {
                        out.push(',');
                    }
                    write_json_string(out, k);
                    out.push(':');
                    v.write_compact(out);
                }
                out.push('}');
            }
        }
    }

    fn write_pretty(&self, out: &mut String, depth: usize, pad: &str) {
        match self {
            Json::Array(items) if !items.is_empty() => {
                out.push('[');
                for (i, v) in items.iter().enumerate() {
                    if i > 0 {
                        out.push(',');
                    }
                    out.push('\n');
                    push_indent(out, depth + 1, pad);
                    v.write_pretty(out, depth + 1, pad);
                }
                out.push('\n');
                push_indent(out, depth, pad);
                out.push(']');
            }
            Json::Object(members) if !members.is_empty() => {
                out.push('{');
                for (i, (k, v)) in members.iter().enumerate() {
                    if i > 0 {
                        out.push(',');
                    }
                    out.push('\n');
                    push_indent(out, depth + 1, pad);
                    write_json_string(out, k);
                    out.push_str(": ");
                    v.write_pretty(out, depth + 1, pad);
                }
                out.push('\n');
                push_indent(out, depth, pad);
                out.push('}');
            }
            // Scalars and empty containers render identically in both modes.
            other => other.write_compact(out),
        }
    }
}

fn push_indent(out: &mut String, depth: usize, pad: &str) {
    for _ in 0..depth {
        out.push_str(pad);
    }
}

/// Emit a JSON string literal, escaping per RFC 8259. Like JSON.stringify it
/// leaves '/' unescaped and emits control characters below U+0020 as \uXXXX.
fn write_json_string(out: &mut String, s: &str) {
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            '\u{08}' => out.push_str("\\b"),
            '\u{0c}' => out.push_str("\\f"),
            c if (c as u32) < 0x20 => {
                let _ = write!(out, "\\u{:04x}", c as u32);
            }
            c => out.push(c),
        }
    }
    out.push('"');
}

/// A minimal recursive-descent JSON parser that builds a Json value tree.
struct Parser<'a> {
    bytes: &'a [u8],
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(text: &'a str) -> Self {
        Self { bytes: text.as_bytes(), pos: 0 }
    }
    fn err(&self, message: &'static str) -> ParseError {
        ParseError { offset: self.pos, message }
    }
    fn peek(&self) -> Option<u8> {
        self.bytes.get(self.pos).copied()
    }
    fn bump(&mut self) {
        self.pos += 1;
    }
    /// Advance one UTF-8 codepoint (leading byte + continuation bytes). Only
    /// called on a valid &str, so this never splits a codepoint.
    fn bump_rune(&mut self) {
        self.pos += 1;
        while matches!(self.peek(), Some(b) if (b & 0xC0) == 0x80) {
            self.pos += 1;
        }
    }
    fn skip_ws(&mut self) {
        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
            self.pos += 1;
        }
    }
    fn parse_document(&mut self) -> Result<Json, ParseError> {
        self.skip_ws();
        let v = self.parse_value()?;
        self.skip_ws();
        if self.peek().is_some() {
            return Err(self.err("unexpected trailing characters"));
        }
        Ok(v)
    }
    fn parse_value(&mut self) -> Result<Json, ParseError> {
        match self.peek() {
            Some(b'{') => self.parse_object(),
            Some(b'[') => self.parse_array(),
            Some(b'"') => Ok(Json::Str(self.parse_string()?)),
            Some(b't') | Some(b'f') => self.parse_bool(),
            Some(b'n') => self.parse_null(),
            Some(b'-') | Some(b'0'..=b'9') => self.parse_number(),
            _ => Err(self.err("unexpected token")),
        }
    }
    fn parse_object(&mut self) -> Result<Json, ParseError> {
        self.bump(); // consume '{'
        self.skip_ws();
        let mut members = Vec::new();
        if self.peek() == Some(b'}') {
            self.bump();
            return Ok(Json::Object(members));
        }
        loop {
            self.skip_ws();
            if self.peek() != Some(b'"') {
                return Err(self.err("expected string key"));
            }
            let key = self.parse_string()?;
            self.skip_ws();
            if self.peek() != Some(b':') {
                return Err(self.err("expected ':' after key"));
            }
            self.bump();
            self.skip_ws();
            let value = self.parse_value()?;
            members.push((key, value));
            self.skip_ws();
            match self.peek() {
                Some(b',') => self.bump(),
                Some(b'}') => {
                    self.bump();
                    return Ok(Json::Object(members));
                }
                _ => return Err(self.err("expected ',' or '}'")),
            }
        }
    }
    fn parse_array(&mut self) -> Result<Json, ParseError> {
        self.bump(); // consume '['
        self.skip_ws();
        let mut items = Vec::new();
        if self.peek() == Some(b']') {
            self.bump();
            return Ok(Json::Array(items));
        }
        loop {
            self.skip_ws();
            items.push(self.parse_value()?);
            self.skip_ws();
            match self.peek() {
                Some(b',') => self.bump(),
                Some(b']') => {
                    self.bump();
                    return Ok(Json::Array(items));
                }
                _ => return Err(self.err("expected ',' or ']'")),
            }
        }
    }
    fn parse_string(&mut self) -> Result<String, ParseError> {
        self.bump(); // opening '"'
        let mut s = String::new();
        loop {
            match self.peek() {
                None => return Err(self.err("unterminated string")),
                Some(b'"') => {
                    self.bump();
                    return Ok(s);
                }
                Some(b'\\') => {
                    self.bump();
                    self.parse_escape(&mut s)?;
                }
                Some(c) if c <= 0x1F => {
                    return Err(self.err("unescaped control character in string"));
                }
                Some(_) => {
                    let start = self.pos;
                    self.bump_rune();
                    // Input is a valid &str, so the consumed slice is UTF-8.
                    s.push_str(std::str::from_utf8(&self.bytes[start..self.pos]).unwrap());
                }
            }
        }
    }
    fn parse_escape(&mut self, s: &mut String) -> Result<(), ParseError> {
        match self.peek() {
            Some(b'"') => { self.bump(); s.push('"'); Ok(()) }
            Some(b'\\') => { self.bump(); s.push('\\'); Ok(()) }
            Some(b'/') => { self.bump(); s.push('/'); Ok(()) }
            Some(b'b') => { self.bump(); s.push('\u{08}'); Ok(()) }
            Some(b'f') => { self.bump(); s.push('\u{0c}'); Ok(()) }
            Some(b'n') => { self.bump(); s.push('\n'); Ok(()) }
            Some(b'r') => { self.bump(); s.push('\r'); Ok(()) }
            Some(b't') => { self.bump(); s.push('\t'); Ok(()) }
            Some(b'u') => { self.bump(); let cp = self.parse_hex4()?; self.decode_surrogate(s, cp) }
            _ => Err(self.err("invalid escape sequence")),
        }
    }
    /// Enforce correct UTF-16 surrogate pairing for '\u' escapes.
    fn decode_surrogate(&mut self, s: &mut String, cp: u16) -> Result<(), ParseError> {
        match cp {
            0xD800..=0xDBFF => {
                if self.peek() != Some(b'\\') {
                    return Err(self.err("dangling high surrogate"));
                }
                self.bump();
                if self.peek() != Some(b'u') {
                    return Err(self.err("expected '\\u' for surrogate pair"));
                }
                self.bump();
                let lo = self.parse_hex4()?;
                match lo {
                    0xDC00..=0xDFFF => {
                        let scalar = 0x10000
                            + (((cp as u32 - 0xD800) << 10) | (lo as u32 - 0xDC00));
                        if let Some(ch) = char::from_u32(scalar) {
                            s.push(ch);
                        }
                        Ok(())
                    }
                    _ => Err(self.err("invalid low surrogate after high surrogate")),
                }
            }
            0xDC00..=0xDFFF => Err(self.err("unexpected low surrogate")),
            _ => {
                if let Some(ch) = char::from_u32(cp as u32) {
                    s.push(ch);
                }
                Ok(())
            }
        }
    }
    /// Read exactly four hexadecimal digits following a '\u'.
    fn parse_hex4(&mut self) -> Result<u16, ParseError> {
        let mut value: u16 = 0;
        for _ in 0..4 {
            let b = match self.peek() {
                Some(b) => b,
                None => return Err(self.err("incomplete '\\u' escape")),
            };
            let d = match b {
                b'0'..=b'9' => b - b'0',
                b'a'..=b'f' => b - b'a' + 10,
                b'A'..=b'F' => b - b'A' + 10,
                _ => return Err(self.err("invalid hex digit in '\\u' escape")),
            };
            value = value * 16 + d as u16;
            self.bump();
        }
        Ok(value)
    }
    fn parse_number(&mut self) -> Result<Json, ParseError> {
        let start = self.pos;
        if self.peek() == Some(b'-') {
            self.bump();
        }
        match self.peek() {
            Some(b'0') => self.bump(),
            Some(b'1'..=b'9') => {
                self.bump();
                while matches!(self.peek(), Some(b'0'..=b'9')) {
                    self.bump();
                }
            }
            _ => return Err(ParseError { offset: start, message: "invalid number" }),
        }
        if self.peek() == Some(b'.') {
            self.bump();
            if !matches!(self.peek(), Some(b'0'..=b'9')) {
                return Err(self.err("expected digit after decimal point"));
            }
            while matches!(self.peek(), Some(b'0'..=b'9')) {
                self.bump();
            }
        }
        if matches!(self.peek(), Some(b'e') | Some(b'E')) {
            self.bump();
            if matches!(self.peek(), Some(b'+') | Some(b'-')) {
                self.bump();
            }
            if !matches!(self.peek(), Some(b'0'..=b'9')) {
                return Err(self.err("expected digit in exponent"));
            }
            while matches!(self.peek(), Some(b'0'..=b'9')) {
                self.bump();
            }
        }
        let text = std::str::from_utf8(&self.bytes[start..self.pos]).unwrap();
        match text.parse::<f64>() {
            Ok(n) => Ok(Json::Number(n)),
            Err(_) => Err(self.err("invalid number")),
        }
    }
    fn parse_bool(&mut self) -> Result<Json, ParseError> {
        if self.consume(b"true") {
            Ok(Json::Bool(true))
        } else if self.consume(b"false") {
            Ok(Json::Bool(false))
        } else {
            Err(self.err("invalid literal"))
        }
    }
    fn parse_null(&mut self) -> Result<Json, ParseError> {
        if self.consume(b"null") {
            Ok(Json::Null)
        } else {
            Err(self.err("invalid literal"))
        }
    }
    /// Match a literal keyword at the current position; on success advance.
    fn consume(&mut self, kw: &[u8]) -> bool {
        if self.bytes.get(self.pos..self.pos + kw.len()) == Some(kw) {
            self.pos += kw.len();
            true
        } else {
            false
        }
    }
}

fn main() {
    let sample = r#"{"name":"CosmoDev","tools":["json","base64"],"free":true,"nested":{"ok":1}}"#;
    match format_json(sample, Mode::Beautify, 2) {
        FormatResult { error: Some(e), .. } => println!("error: {e}"),
        FormatResult { output, .. } => println!("{output}"),
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →