Skip to content

JSON Formatter — Java source

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

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

// json-formatter — parse, pretty-print (2-space indent), and minify.
//
// Language: Java (Java 17, standard library only — the JDK ships no public
//            JSON API; the ecosystem equivalents are Jackson or Gson)
// Source:   CosmoDev polyglot showcase port of the `json-formatter` tool,
//           ported from src/tools/JsonFormatter.tsx (the canonical
//           TypeScript island). In a real project this class lives in
//           JsonFormatter.java; it is named java.java for the polyglot
//           layout.
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Mirroring the Rust port, this file includes a small hand-written
// recursive-descent parser and serializer. Object members are stored in a
// List<Member>, which preserves source insertion order just like a
// JavaScript engine.
//
// Parity caveats: numbers are held as double (matching JS Number), so
// integers beyond 2^53 lose precision, and Double#toString keeps a
// trailing ".0" / switches to 'E' notation at different thresholds than
// ECMAScript's Number::toString (integral values below 1e15 are printed as
// plain integers to match the common cases). String content is iterated as
// UTF-16 chars, so lone surrogates in raw (non-escaped) input pass through
// unchanged.

import java.util.ArrayList;
import java.util.List;

public final class JsonFormatter {

    private JsonFormatter() {}

    /** Selects the output shape. */
    public enum Mode { BEAUTIFY, MINIFY }

    /** Mirrors the TypeScript { output, error } shape; error == null means
     *  success or empty input. */
    public record FormatResult(String output, String error) {}

    /** A parsed JSON value. Object members preserve source insertion order. */
    public sealed interface Json permits Null, Bool, Num, Str, Arr, Obj {}
    public record Null() implements Json {}
    public record Bool(boolean value) implements Json {}
    public record Num(double value) implements Json {}
    public record Str(String value) implements Json {}
    public record Arr(List<Json> items) implements Json {}
    public record Obj(List<Member> members) implements Json {}
    public record Member(String key, Json value) {}

    /** A parse failure: byte offset + message (checked, like
     *  java.text.ParseException). */
    public static final class ParseException extends Exception {
        public final int offset;

        ParseException(int offset, String message) {
            super(message);
            this.offset = offset;
        }
    }

    /**
     * Parse and re-emit a JSON document the way the live island does.
     * {@code indent} is the number of spaces per nesting level for
     * {@code BEAUTIFY} (pass 2 to match the UI). Empty/whitespace-only
     * input yields empty output, no error.
     */
    public static FormatResult formatJson(String input, Mode mode, int indent) {
        String raw = input.strip();
        if (raw.isEmpty()) return new FormatResult("", null);

        Json value;
        try {
            value = new Parser(raw).parseDocument();
        } catch (ParseException e) {
            return new FormatResult("", e.getMessage() + " at byte " + e.offset);
        }

        String pad = " ".repeat(indent);
        StringBuilder out = new StringBuilder();
        if (mode == Mode.MINIFY) {
            writeCompact(value, out);
        } else {
            writePretty(value, out, 0, pad);
        }
        return new FormatResult(out.toString(), null);
    }

    // -----------------------------------------------------------------
    // Parser — a minimal recursive-descent JSON parser.
    // -----------------------------------------------------------------

    static final class Parser {
        private final String text;
        private int pos;

        Parser(String text) { this.text = text; }

        ParseException fail(String message) { return new ParseException(pos, message); }

        private char peek() { return pos < text.length() ? text.charAt(pos) : '\0'; }

        private static boolean isDigit(char c) { return c >= '0' && c <= '9'; }

        private void skipWs() {
            while (peek() == ' ' || peek() == '\t' || peek() == '\n' || peek() == '\r') pos++;
        }

        /** Match a literal keyword at the current position; on success advance. */
        private boolean matchKw(String kw) {
            if (text.regionMatches(pos, kw, 0, kw.length())) {
                pos += kw.length();
                return true;
            }
            return false;
        }

        Json parseDocument() throws ParseException {
            skipWs();
            Json value = parseValue();
            skipWs();
            if (pos < text.length()) throw fail("unexpected trailing characters");
            return value;
        }

        private Json parseValue() throws ParseException {
            char c = peek();
            if (c == '-' || isDigit(c)) return parseNumber();
            switch (c) {
                case '{': return parseObject();
                case '[': return parseArray();
                case '"': return new Str(parseString());
                case 't': case 'f': return parseBool();
                case 'n': return parseNull();
                default: throw fail("unexpected token");
            }
        }

        private Json parseObject() throws ParseException {
            pos++; // consume '{'
            skipWs();
            List<Member> members = new ArrayList<>();
            if (peek() == '}') { pos++; return new Obj(members); }
            while (true) {
                skipWs();
                if (peek() != '"') throw fail("expected string key");
                String key = parseString();
                skipWs();
                if (peek() != ':') throw fail("expected ':' after key");
                pos++;
                skipWs();
                members.add(new Member(key, parseValue()));
                skipWs();
                if (peek() == ',') { pos++; continue; }
                if (peek() == '}') { pos++; return new Obj(members); }
                throw fail("expected ',' or '}'");
            }
        }

        private Json parseArray() throws ParseException {
            pos++; // consume '['
            skipWs();
            List<Json> items = new ArrayList<>();
            if (peek() == ']') { pos++; return new Arr(items); }
            while (true) {
                skipWs();
                items.add(parseValue());
                skipWs();
                if (peek() == ',') { pos++; continue; }
                if (peek() == ']') { pos++; return new Arr(items); }
                throw fail("expected ',' or ']'");
            }
        }

        private String parseString() throws ParseException {
            pos++; // opening '"'
            StringBuilder s = new StringBuilder();
            while (true) {
                char c = peek();
                if (c == '\0') throw fail("unterminated string");
                if (c == '"') { pos++; return s.toString(); }
                if (c == '\\') {
                    pos++;
                    parseEscape(s);
                } else if (c < 0x20) {
                    throw fail("unescaped control character in string");
                } else {
                    s.append(c);
                    pos++;
                }
            }
        }

        private void parseEscape(StringBuilder s) throws ParseException {
            char c = peek();
            switch (c) {
                case '"' -> { pos++; s.append('"'); }
                case '\\' -> { pos++; s.append('\\'); }
                case '/' -> { pos++; s.append('/'); }
                case 'b' -> { pos++; s.append('\b'); }
                case 'f' -> { pos++; s.append('\f'); }
                case 'n' -> { pos++; s.append('\n'); }
                case 'r' -> { pos++; s.append('\r'); }
                case 't' -> { pos++; s.append('\t'); }
                case 'u' -> { pos++; parseUnicodeEscape(s); }
                default -> throw fail("invalid escape sequence");
            }
        }

        // Enforce correct UTF-16 surrogate pairing for \\u escapes.
        // (Doubled backslash: javac lexes unicode escapes even in comments.)
        private void parseUnicodeEscape(StringBuilder s) throws ParseException {
            int cp = parseHex4();
            if (cp >= 0xD800 && cp <= 0xDBFF) {
                if (peek() != '\\') throw fail("dangling high surrogate");
                pos++;
                if (peek() != 'u') throw fail("expected '\\u' for surrogate pair");
                pos++;
                int lo = parseHex4();
                if (lo < 0xDC00 || lo > 0xDFFF)
                    throw fail("invalid low surrogate after high surrogate");
                s.appendCodePoint(0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00));
            } else if (cp >= 0xDC00 && cp <= 0xDFFF) {
                throw fail("unexpected low surrogate");
            } else {
                s.appendCodePoint(cp);
            }
        }

        // Read exactly four hexadecimal digits following a \\u escape.
        private int parseHex4() throws ParseException {
            int value = 0;
            for (int i = 0; i < 4; i++) {
                char c = peek();
                int d;
                if (c >= '0' && c <= '9') d = c - '0';
                else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
                else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
                else throw fail("invalid hex digit in '\\u' escape");
                value = value * 16 + d;
                pos++;
            }
            return value;
        }

        private Json parseNumber() throws ParseException {
            int start = pos;
            if (peek() == '-') pos++;
            if (peek() == '0') {
                pos++;
            } else if (peek() >= '1' && peek() <= '9') {
                while (isDigit(peek())) pos++;
            } else {
                throw new ParseException(start, "invalid number");
            }
            if (peek() == '.') {
                pos++;
                if (!isDigit(peek())) throw fail("expected digit after decimal point");
                while (isDigit(peek())) pos++;
            }
            if (peek() == 'e' || peek() == 'E') {
                pos++;
                if (peek() == '+' || peek() == '-') pos++;
                if (!isDigit(peek())) throw fail("expected digit in exponent");
                while (isDigit(peek())) pos++;
            }
            try {
                return new Num(Double.parseDouble(text.substring(start, pos)));
            } catch (NumberFormatException e) {
                throw fail("invalid number");
            }
        }

        private Json parseBool() throws ParseException {
            if (matchKw("true")) return new Bool(true);
            if (matchKw("false")) return new Bool(false);
            throw fail("invalid literal");
        }

        private Json parseNull() throws ParseException {
            if (matchKw("null")) return new Null();
            throw fail("invalid literal");
        }
    }

    // -----------------------------------------------------------------
    // Serializer.
    // -----------------------------------------------------------------

    private static void pushIndent(StringBuilder out, int depth, String pad) {
        out.append(pad.repeat(depth));
    }

    /** Emit a JSON string literal, escaping per RFC 8259. Like
     *  JSON.stringify it leaves '/' unescaped and emits control characters
     *  below U+0020 as \\uXXXX. */
    private static void writeJsonString(String value, StringBuilder out) {
        out.append('"');
        value.codePoints().forEach(cp -> {
            switch (cp) {
                case '"' -> out.append("\\\"");
                case '\\' -> out.append("\\\\");
                case '\n' -> out.append("\\n");
                case '\r' -> out.append("\\r");
                case '\t' -> out.append("\\t");
                case '\b' -> out.append("\\b");
                case '\f' -> out.append("\\f");
                default -> {
                    if (cp < 0x20) out.append(String.format("\\u%04x", cp));
                    else out.appendCodePoint(cp);
                }
            }
        });
        out.append('"');
    }

    /** Print a double the way JavaScript most often renders it: integral
     *  values below 1e15 without a trailing ".0", everything else via
     *  Double#toString. */
    private static void writeNumber(double v, StringBuilder out) {
        if (v == Math.rint(v) && Math.abs(v) < 1e15) {
            out.append((long) v);
        } else {
            out.append(v);
        }
    }

    private static void writeCompact(Json value, StringBuilder out) {
        if (value instanceof Null) {
            out.append("null");
        } else if (value instanceof Bool b) {
            out.append(b.value() ? "true" : "false");
        } else if (value instanceof Num n) {
            writeNumber(n.value(), out);
        } else if (value instanceof Str s) {
            writeJsonString(s.value(), out);
        } else if (value instanceof Arr a) {
            out.append('[');
            List<Json> items = a.items();
            for (int i = 0; i < items.size(); i++) {
                if (i > 0) out.append(',');
                writeCompact(items.get(i), out);
            }
            out.append(']');
        } else if (value instanceof Obj o) {
            out.append('{');
            List<Member> members = o.members();
            for (int i = 0; i < members.size(); i++) {
                if (i > 0) out.append(',');
                writeJsonString(members.get(i).key(), out);
                out.append(':');
                writeCompact(members.get(i).value(), out);
            }
            out.append('}');
        }
    }

    private static void writePretty(Json value, StringBuilder out, int depth, String pad) {
        if (value instanceof Arr a && !a.items().isEmpty()) {
            out.append('[');
            List<Json> items = a.items();
            for (int i = 0; i < items.size(); i++) {
                if (i > 0) out.append(',');
                out.append('\n');
                pushIndent(out, depth + 1, pad);
                writePretty(items.get(i), out, depth + 1, pad);
            }
            out.append('\n');
            pushIndent(out, depth, pad);
            out.append(']');
        } else if (value instanceof Obj o && !o.members().isEmpty()) {
            out.append('{');
            List<Member> members = o.members();
            for (int i = 0; i < members.size(); i++) {
                if (i > 0) out.append(',');
                out.append('\n');
                pushIndent(out, depth + 1, pad);
                writeJsonString(members.get(i).key(), out);
                out.append(": ");
                writePretty(members.get(i).value(), out, depth + 1, pad);
            }
            out.append('\n');
            pushIndent(out, depth, pad);
            out.append('}');
        } else {
            // Scalars and empty containers render identically in both modes.
            writeCompact(value, out);
        }
    }

    // -----------------------------------------------------------------
    // Demo.
    // -----------------------------------------------------------------

    public static void main(String[] args) {
        String sample = "{\"name\":\"CosmoDev\",\"tools\":[\"json\",\"base64\"],"
                + "\"free\":true,\"nested\":{\"ok\":1}}";
        FormatResult result = formatJson(sample, Mode.BEAUTIFY, 2);
        System.out.println(result.error() != null ? result.error() : result.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 →