Skip to content

JSON Minify — Rust source

Remove all whitespace from JSON for the most compact serialization. Validates as it minifies, runs entirely in your browser, with a shareable link to your exact input.

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

//! json-minify — compact JSON to its smallest valid form.
//!
//! Language: Rust (edition 2021, standard library only)
//! Source:   CosmoDev polyglot showcase port of the JSON Minify tool, ported
//!           from src/lib/json-minify.ts (the canonical TypeScript implementation)
//!           and cli/json-minify/json-minify.go (the live Go CLI twin).
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Pure + deterministic; never panics (invalid JSON -> None).
//!   - Functionally equivalent to the TS / Go references: same inputs -> same
//!     outputs.
//!   - Self-contained: std only (no crates.io dependencies — no `serde_json`).
//!
//! Approach: unlike the TS/Go/Python/PHP/JS ports, which lean on the language's
//! built-in JSON codec, Rust's std has none and the brief forbids external
//! crates. So this is a hand-rolled recursive-descent JSON parser that
//! VALIDATES (rejecting invalid input, like Go's json.Compact) and EMITS the
//! compact form as it walks — stripping every insignificant space, tab, CR,
//! and LF while copying strings, numbers, and structural tokens verbatim. The
//! output is therefore byte-for-byte stable for a given input, with no
//! dependence on float reformatting or key reordering.
//!
//! JSON grammar implemented: RFC 8259. Whitespace (U+0020, U+0009, U+000A,
//! U+000D) is the only thing dropped; everything else is validated-and-copied
//! or rejected.

/// Compact JSON serialization of `input`, or `None` if it is not valid JSON.
///
/// Mirrors TS `minifyJson` and Go `Minify`. Never panics.
pub fn minify(input: &str) -> Option<String> {
    let chars: Vec<char> = input.chars().collect();
    let mut p = Parser { chars: &chars, pos: 0 };
    let mut out = String::with_capacity(input.len());

    p.skip_ws();
    p.parse_value(&mut out).ok()?;
    // Reject anything (garbage) left after the single top-level value.
    if p.skip_ws_to_end() {
        Some(out)
    } else {
        None
    }
}

/// Minimal cursor over a slice of chars. Carries no allocation beyond the
/// caller-owned output buffer; the only error channel is `Result<(), ()>`.
struct Parser<'a> {
    chars: &'a [char],
    pos: usize,
}

impl<'a> Parser<'a> {
    #[inline]
    fn peek(&self) -> Option<char> {
        self.chars.get(self.pos).copied()
    }

    /// Consume and return the next char, if any.
    #[inline]
    fn bump(&mut self) -> Option<char> {
        let c = self.peek();
        if c.is_some() {
            self.pos += 1;
        }
        c
    }

    /// Skip RFC 8259 insignificant whitespace. Returns whether the cursor then
    /// sits at end-of-input (i.e. only whitespace remained).
    fn skip_ws_to_end(&mut self) -> bool {
        while let Some(c) = self.peek() {
            if matches!(c, ' ' | '\t' | '\n' | '\r') {
                self.pos += 1;
            } else {
                return false;
            }
        }
        true
    }

    fn skip_ws(&mut self) {
        let _ = self.skip_ws_to_end();
    }

    /// Parse a single JSON value, appending its compact form to `out`.
    fn parse_value(&mut self, out: &mut String) -> Result<(), ()> {
        self.skip_ws();
        match self.peek() {
            Some('{') => self.parse_object(out),
            Some('[') => self.parse_array(out),
            Some('"') => self.parse_string(out),
            Some('t') => self.parse_literal(out, "true"),
            Some('f') => self.parse_literal(out, "false"),
            Some('n') => self.parse_literal(out, "null"),
            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(out),
            _ => Err(()),
        }
    }

    fn parse_object(&mut self, out: &mut String) -> Result<(), ()> {
        out.push('{');
        self.pos += 1; // consume '{'
        self.skip_ws();
        if self.peek() == Some('}') {
            self.pos += 1;
            out.push('}');
            return Ok(());
        }
        loop {
            self.skip_ws();
            // Keys MUST be strings.
            if self.peek() != Some('"') {
                return Err(());
            }
            self.parse_string(out)?;
            self.skip_ws();
            if self.bump() != Some(':') {
                return Err(());
            }
            out.push(':');
            self.parse_value(out)?;
            self.skip_ws();
            match self.bump() {
                Some(',') => out.push(','),
                Some('}') => {
                    out.push('}');
                    return Ok(());
                }
                _ => return Err(()),
            }
        }
    }

    fn parse_array(&mut self, out: &mut String) -> Result<(), ()> {
        out.push('[');
        self.pos += 1; // consume '['
        self.skip_ws();
        if self.peek() == Some(']') {
            self.pos += 1;
            out.push(']');
            return Ok(());
        }
        loop {
            self.parse_value(out)?;
            self.skip_ws();
            match self.bump() {
                Some(',') => out.push(','),
                Some(']') => {
                    out.push(']');
                    return Ok(());
                }
                _ => return Err(()),
            }
        }
    }

    /// Parse a string, copying it verbatim (including any escape sequences)
    /// while validating structure. Rejects unterminated strings and unescaped
    /// control characters (U+0000..U+001F).
    fn parse_string(&mut self, out: &mut String) -> Result<(), ()> {
        debug_assert_eq!(self.peek(), Some('"'));
        out.push('"');
        self.pos += 1;
        loop {
            match self.bump() {
                None => return Err(()), // unterminated
                Some('"') => {
                    out.push('"');
                    return Ok(());
                }
                Some('\\') => {
                    out.push('\\');
                    match self.bump() {
                        Some(esc @ ('"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't')) => out.push(esc),
                        Some('u') => {
                            out.push('u');
                            // Exactly four hex digits must follow \u.
                            for _ in 0..4 {
                                match self.bump() {
                                    Some(h) if h.is_ascii_hexdigit() => out.push(h),
                                    _ => return Err(()),
                                }
                            }
                        }
                        _ => return Err(()),
                    }
                }
                Some(c) if (c as u32) < 0x20 => return Err(()), // unescaped control char
                Some(c) => out.push(c),
            }
        }
    }

    /// Parse a JSON number, copying its source token verbatim after validating
    /// the grammar: an optional leading '-', a non-zero-prefixed integer part,
    /// and an optional fraction / exponent. Rejects a bare '-', leading zeros
    /// like '01', and a '.' or 'e' with no following digits.
    fn parse_number(&mut self, out: &mut String) -> Result<(), ()> {
        let start = self.pos;
        if self.peek() == Some('-') {
            self.pos += 1;
        }
        // Integer part: '0' alone, or [1-9] then further digits.
        match self.peek() {
            Some('0') => self.pos += 1,
            Some(c) if ('1'..='9').contains(&c) => {
                self.pos += 1;
                self.consume_digits();
            }
            _ => return Err(()),
        }
        // Optional fraction: '.' then one+ digits.
        if self.peek() == Some('.') {
            self.pos += 1;
            if !self.consume_digits() {
                return Err(()); // '.' with no digits
            }
        }
        // Optional exponent: [eE], optional [+-], then one+ digits.
        if matches!(self.peek(), Some('e') | Some('E')) {
            self.pos += 1;
            if matches!(self.peek(), Some('+') | Some('-')) {
                self.pos += 1;
            }
            if !self.consume_digits() {
                return Err(()); // 'e' with no digits
            }
        }
        out.extend(self.chars[start..self.pos].iter().copied());
        Ok(())
    }

    /// Consume a run of ASCII digits; returns whether any were consumed.
    fn consume_digits(&mut self) -> bool {
        let mut any = false;
        while let Some(c) = self.peek() {
            if c.is_ascii_digit() {
                self.pos += 1;
                any = true;
            } else {
                break;
            }
        }
        any
    }

    /// Match a fixed keyword token (true/false/null), copying it on success.
    fn parse_literal(&mut self, out: &mut String, lit: &str) -> Result<(), ()> {
        for expected in lit.chars() {
            if self.bump() != Some(expected) {
                return Err(());
            }
        }
        out.push_str(lit);
        Ok(())
    }
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn strips_whitespace() {
        assert_eq!(
            minify("{\n  \"a\": 1,\n  \"b\": [\n    1,\n    2\n  ]\n}"),
            Some("{\"a\":1,\"b\":[1,2]}".to_string())
        );
    }

    #[test]
    fn nested_collapses() {
        assert_eq!(
            minify("{ \"x\": { \"y\": [1, 2, 3] } }"),
            Some("{\"x\":{\"y\":[1,2,3]}}".to_string())
        );
    }

    #[test]
    fn scalars_and_passthrough() {
        // Already-compact input passes through; scalars are valid top-level JSON.
        assert_eq!(minify("{\"a\":1}"), Some("{\"a\":1}".to_string()));
        assert_eq!(minify("123"), Some("123".to_string()));
        assert_eq!(minify("true"), Some("true".to_string()));
        assert_eq!(minify("null"), Some("null".to_string()));
    }

    #[test]
    fn unicode_preserved() {
        // Non-ASCII is copied verbatim (never \u-escaped), matching Go/JS.
        assert_eq!(minify("\"héllo 🚀\""), Some("\"héllo 🚀\"".to_string()));
    }

    #[test]
    fn invalid_returns_none() {
        assert_eq!(minify("{invalid}"), None);
        assert_eq!(minify(""), None);
        assert_eq!(minify("[1, 2,"), None);
    }
}

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 →