Skip to content

JSON Formatter — Kotlin source

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

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

// json-formatter — parse, pretty-print (2-space indent), and minify.
//
// Language: Kotlin (Kotlin 1.9, standard library only — the ecosystem
//            equivalent is kotlinx.serialization-json)
// Source:   CosmoDev polyglot showcase port of the `json-formatter` tool,
//           ported from src/tools/JsonFormatter.tsx (the canonical
//           TypeScript island).
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Mirroring the Rust and Java ports (both stdlib-only), this file includes
// a small hand-written recursive-descent parser and serializer instead of
// pulling in kotlinx.serialization. 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 Kotlin's 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 kotlin.math.abs

/** A parsed JSON value. Object members preserve source insertion order. */
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 members: List<Member>) : Json
}

data class Member(val key: String, val value: Json)

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

/** Mirrors the TypeScript { output, error } shape; error == null means
 *  success or empty input. */
data class FormatResult(val output: String, val error: String? = null)

/** A parse failure; the message carries the char position, matching the
 *  engine-message feel of the island's `e.message`. */
class ParseException(message: String) : Exception(message)

/**
 * Parse and re-emit a JSON document the way the live island does.
 *
 * - empty / whitespace-only input -> empty output, no error
 * - valid JSON + MINIFY   -> compact JSON (no whitespace)
 * - valid JSON + BEAUTIFY -> indent-space-indented JSON
 * - invalid JSON          -> empty output, error message
 *
 * @param indent spaces per nesting level for BEAUTIFY (the UI passes 2)
 */
fun formatJson(input: String, mode: Mode = Mode.BEAUTIFY, indent: Int = 2): FormatResult {
    val raw = input.trim()
    if (raw.isEmpty()) return FormatResult("")

    val value = try {
        Parser(raw).parseDocument()
    } catch (e: ParseException) {
        return FormatResult("", e.message)
    }

    val pad = " ".repeat(indent)
    val out = buildString {
        when (mode) {
            Mode.MINIFY -> value.writeCompact(this)
            Mode.BEAUTIFY -> value.writePretty(this, 0, pad)
        }
    }
    return FormatResult(out)
}

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

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

    private fun fail(message: String): Nothing = throw ParseException("$message at char $pos")

    /** '\0' means end of input (never a JSON whitespace match, so loops
     *  that test peek() terminate). */
    private fun peek(): Char = if (pos < text.length) text[pos] else '\u0000'

    private fun isDigit(c: Char) = c in '0'..'9'

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

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

    fun parseDocument(): Json {
        skipWs()
        val value = parseValue()
        skipWs()
        if (pos < text.length) fail("unexpected trailing characters")
        return value
    }

    private fun parseValue(): Json {
        val c = peek()
        if (c == '-' || isDigit(c)) return parseNumber()
        return when (c) {
            '{' -> parseObject()
            '[' -> parseArray()
            '"' -> Json.Str(parseString())
            't', 'f' -> parseBool()
            'n' -> parseNull()
            else -> fail("unexpected token")
        }
    }

    private fun parseObject(): Json {
        pos++ // consume '{'
        skipWs()
        val members = mutableListOf<Member>()
        if (peek() == '}') { pos++; return Json.Obj(members) }
        while (true) {
            skipWs()
            if (peek() != '"') fail("expected string key")
            val key = parseString()
            skipWs()
            if (peek() != ':') fail("expected ':' after key")
            pos++
            skipWs()
            members.add(Member(key, parseValue()))
            skipWs()
            when (peek()) {
                ',' -> pos++
                '}' -> { pos++; return Json.Obj(members) }
                else -> fail("expected ',' or '}'")
            }
        }
    }

    private fun parseArray(): Json {
        pos++ // consume '['
        skipWs()
        val items = mutableListOf<Json>()
        if (peek() == ']') { pos++; return Json.Arr(items) }
        while (true) {
            skipWs()
            items.add(parseValue())
            skipWs()
            when (peek()) {
                ',' -> pos++
                ']' -> { pos++; return Json.Arr(items) }
                else -> fail("expected ',' or ']'")
            }
        }
    }

    private fun parseString(): String {
        pos++ // opening '"'
        val s = StringBuilder()
        while (true) {
            val c = peek()
            when {
                c == '\u0000' -> fail("unterminated string")
                c == '"' -> { pos++; return s.toString() }
                c == '\\' -> { pos++; parseEscape(s) }
                c < ' ' -> fail("unescaped control character in string")
                else -> { s.append(c); pos++ }
            }
        }
    }

    private fun parseEscape(s: StringBuilder) {
        when (peek()) {
            '"' -> { pos++; s.append('"') }
            '\\' -> { pos++; s.append('\\') }
            '/' -> { pos++; s.append('/') }
            'b' -> { pos++; s.append('\b') }
            'f' -> { pos++; s.append('\u000C') }
            'n' -> { pos++; s.append('\n') }
            'r' -> { pos++; s.append('\r') }
            't' -> { pos++; s.append('\t') }
            'u' -> { pos++; parseUnicodeEscape(s) }
            else -> fail("invalid escape sequence")
        }
    }

    /** Enforce correct UTF-16 surrogate pairing for '\u' escapes. */
    private fun parseUnicodeEscape(s: StringBuilder) {
        val cp = parseHex4()
        if (cp in 0xD800..0xDBFF) {
            if (peek() != '\\') fail("dangling high surrogate")
            pos++
            if (peek() != 'u') fail("expected '\\u' for surrogate pair")
            pos++
            val lo = parseHex4()
            if (lo !in 0xDC00..0xDFFF) fail("invalid low surrogate after high surrogate")
            s.appendCodePoint(0x10000 + ((cp - 0xD800) shl 10) + (lo - 0xDC00))
        } else if (cp in 0xDC00..0xDFFF) {
            fail("unexpected low surrogate")
        } else {
            s.appendCodePoint(cp)
        }
    }

    /** Read exactly four hexadecimal digits following a '\u'. */
    private fun parseHex4(): Int {
        var value = 0
        repeat(4) {
            val c = peek()
            val d = when (c) {
                in '0'..'9' -> c - '0'
                in 'a'..'f' -> c - 'a' + 10
                in 'A'..'F' -> c - 'A' + 10
                else -> fail("invalid hex digit in '\\u' escape")
            }
            value = value * 16 + d
            pos++
        }
        return value
    }

    private fun parseNumber(): Json {
        val start = pos
        if (peek() == '-') pos++
        when (peek()) {
            '0' -> pos++
            in '1'..'9' -> while (isDigit(peek())) pos++
            else -> throw ParseException("invalid number at char $start")
        }
        if (peek() == '.') {
            pos++
            if (!isDigit(peek())) fail("expected digit after decimal point")
            while (isDigit(peek())) pos++
        }
        if (peek() == 'e' || peek() == 'E') {
            pos++
            if (peek() == '+' || peek() == '-') pos++
            if (!isDigit(peek())) fail("expected digit in exponent")
            while (isDigit(peek())) pos++
        }
        val slice = text.substring(start, pos)
        return try {
            Json.Num(slice.toDouble())
        } catch (e: NumberFormatException) {
            fail("invalid number")
        }
    }

    private fun parseBool(): Json =
        if (matchKw("true")) Json.Bool(true)
        else if (matchKw("false")) Json.Bool(false)
        else fail("invalid literal")

    private fun parseNull(): Json =
        if (matchKw("null")) Json.Null
        else fail("invalid literal")
}

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

private fun pushIndent(out: StringBuilder, depth: Int, pad: String) {
    repeat(depth) { out.append(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. */
private fun writeJsonString(value: String, out: StringBuilder) {
    out.append('"')
    value.codePoints().forEach { cp ->
        when (cp) {
            '"'.code -> out.append("\\\"")
            '\\'.code -> out.append("\\\\")
            '\n'.code -> out.append("\\n")
            '\r'.code -> out.append("\\r")
            '\t'.code -> out.append("\\t")
            '\b'.code -> out.append("\\b")
            '\u000C'.code -> out.append("\\f")
            else -> {
                if (cp < 0x20) out.append("\\u%04x".format(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 fun writeNumber(v: Double, out: StringBuilder) {
    if (v == Math.rint(v) && abs(v) < 1e15) {
        out.append(v.toLong())
    } else {
        out.append(v)
    }
}

private fun Json.writeCompact(out: StringBuilder) {
    when (this) {
        Json.Null -> out.append("null")
        is Json.Bool -> out.append(if (value) "true" else "false")
        is Json.Num -> writeNumber(value, out)
        is Json.Str -> writeJsonString(value, out)
        is Json.Arr -> {
            out.append('[')
            items.forEachIndexed { i, v ->
                if (i > 0) out.append(',')
                v.writeCompact(out)
            }
            out.append(']')
        }
        is Json.Obj -> {
            out.append('{')
            members.forEachIndexed { i, m ->
                if (i > 0) out.append(',')
                writeJsonString(m.key, out)
                out.append(':')
                m.value.writeCompact(out)
            }
            out.append('}')
        }
    }
}

private fun Json.writePretty(out: StringBuilder, depth: Int, pad: String) {
    when {
        this is Json.Arr && items.isNotEmpty() -> {
            out.append('[')
            items.forEachIndexed { i, v ->
                if (i > 0) out.append(',')
                out.append('\n')
                pushIndent(out, depth + 1, pad)
                v.writePretty(out, depth + 1, pad)
            }
            out.append('\n')
            pushIndent(out, depth, pad)
            out.append(']')
        }
        this is Json.Obj && members.isNotEmpty() -> {
            out.append('{')
            members.forEachIndexed { i, m ->
                if (i > 0) out.append(',')
                out.append('\n')
                pushIndent(out, depth + 1, pad)
                writeJsonString(m.key, out)
                out.append(": ")
                m.value.writePretty(out, depth + 1, pad)
            }
            out.append('\n')
            pushIndent(out, depth, pad)
            out.append('}')
        }
        else -> {
            // Scalars and empty containers render identically in both modes.
            writeCompact(out)
        }
    }
}

fun main() {
    val sample = """{"name":"CosmoDev","tools":["json","base64"],"free":true,"nested":{"ok":1}}"""
    val result = formatJson(sample)
    println(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 →