Skip to content

JSON Validator — Kotlin source

Validate JSON and pinpoint errors with line and column numbers. Clear valid/invalid verdict plus the exact error location - runs entirely in your browser.

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

// json-validator — Kotlin port: hand-rolled recursive-descent validator (the JVM stdlib has no JSON parser), ported from src/lib/jsonValidate.ts.

data class Result(
    val valid: Boolean,
    val error: String? = null,
    val line: Int? = null,   // null = no position reported
    val column: Int? = null,
)

class JsonValidator(private val text: String) {
    private var i = 0

    fun validate(): Result {
        ws()
        if (i >= text.length) return Result(false, "Input is empty")
        var ok = value()
        if (ok) { ws(); ok = i >= text.length } // reject trailing characters
        if (!ok) {
            val (line, col) = lineCol(i) // 1-based; counts "\r\n" as one break
            return Result(false, "Unexpected token", line, col)
        }
        return Result(true)
    }

    private fun ws() { while (i < text.length && text[i] in " \t\r\n") i++ }
    private fun peek(): Char = if (i < text.length) text[i] else '\u0000' // NUL sentinel = EOF
    private fun digit() = i < text.length && text[i] in '0'..'9'

    private fun value(): Boolean = when (peek()) {
        '{' -> obj()
        '[' -> arr()
        '"' -> str()
        't' -> lit("true")
        'f' -> lit("false")
        'n' -> lit("null")
        else -> number()
    }

    // Strings: any escape pair passes; a raw control char (< 0x20) fails.
    private fun str(): Boolean {
        i++ // opening quote
        while (i < text.length && text[i] != '"') {
            if (text[i] == '\\') { if (++i >= text.length) return false }
            else if (text[i] < ' ') return false
            i++
        }
        return i++ < text.length // consume the closing quote, or fail at EOF
    }

    private fun obj(): Boolean {
        i++; ws()
        if (peek() == '}') { i++; return true }
        while (true) {
            ws()
            if (peek() != '"' || !str()) return false
            ws()
            if (peek() != ':') return false
            i++
            if (!value()) return false
            ws()
            when (peek()) {
                ',' -> i++
                '}' -> { i++; return true }
                else -> return false
            }
        }
    }

    private fun arr(): Boolean {
        i++; ws()
        if (peek() == ']') { i++; return true }
        while (true) {
            if (!value()) return false
            ws()
            when (peek()) {
                ',' -> i++
                ']' -> { i++; return true }
                else -> return false
            }
        }
    }

    private fun lit(word: String): Boolean {
        if (!text.startsWith(word, i)) return false
        i += word.length
        return true
    }

    // RFC 8259 number: -?int(.frac)?([eE][+-]?exp)?
    private fun number(): Boolean {
        if (peek() == '-') i++
        if (!digit()) return false
        if (text[i] == '0') i++ else while (digit()) i++
        if (peek() == '.') { i++; if (!digit()) return false; while (digit()) i++ }
        if (peek() == 'e' || peek() == 'E') {
            i++
            if (peek() == '+' || peek() == '-') i++
            if (!digit()) return false
            while (digit()) i++
        }
        return true
    }

    // 1-based line/column for a 0-based offset — the shared polyglot algorithm.
    private fun lineCol(offset: Int): Pair<Int, Int> {
        var line = 1; var col = 1
        var j = 0
        while (j < offset && j < text.length) {
            when (text[j]) {
                '\n' -> { line++; col = 1 }
                '\r' -> { line++; col = 1; if (text.getOrNull(j + 1) == '\n') j++ }
                else -> col++
            }
            j++
        }
        return line to col
    }
}

fun main() {
    for (s in listOf("""{"a": [1, 2, {"b": null}]}""", "{\n  \"a\": ,\n}")) {
        val r = JsonValidator(s).validate()
        println(if (r.valid) "valid" else "invalid: ${r.error} (line ${r.line}, column ${r.column})")
    }
}

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 →