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 →