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 →