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 →