JSON to SQL INSERT — Kotlin source
Convert a JSON array of objects into SQL INSERT statements. Properly escapes strings, handles nulls, booleans, numbers, nested objects, and multi-row inserts.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// json-to-sql — pure JSON → SQL INSERT converter. Kotlin port (canonical TS:
// src/lib/jsonToSql.ts; Go twin: cli/json-to-sql). The JVM stdlib has no JSON
// codec, so — like the Rust port — this carries a small hand-rolled JSON DOM
// parser: LinkedHashMap keeps first-seen key order and number tokens stay raw
// text, so column unions and literals match the TS reference. Errors return
// through the Res data class — never thrown on user input.
sealed class J {
object Nul : J(); data class B(val v: Boolean) : J(); data class Num(val raw: String) : J()
data class S(val s: String) : J()
data class A(val items: List<J>) : J(); data class O(val pairs: LinkedHashMap<String, J>) : J()
}
class Parser(private val s: String) {
private var i = 0
private fun err(): Nothing = throw IllegalArgumentException("Invalid JSON")
private fun peek() = if (i < s.length) s[i] else '\u0000' // '\u0000' sentinel: never a number char
private fun take(): Char { if (i >= s.length) err(); return s[i++] }
private fun ws() { while (i < s.length && s[i].isWhitespace()) i++ }
fun parse(): J { val v = value(); ws(); if (i != s.length) err(); return v }
private fun value(): J { ws(); return when (val c = peek()) {
'{' -> { i++; val m = LinkedHashMap<String, J>(); ws()
if (peek() == '}') { i++; return J.O(m) }
while (true) { ws(); val k = (value() as J.S).s; ws(); if (take() != ':') err()
m[k] = value(); ws(); val d = take(); if (d == '}') break; if (d != ',') err() }
J.O(m) }
'[' -> { i++; val l = mutableListOf<J>(); ws()
if (peek() == ']') { i++; return J.A(l) }
while (true) { l.add(value()); ws(); val d = take(); if (d == ']') break; if (d != ',') err() }
J.A(l) }
'"' -> J.S(str())
'n' -> { i += 4; J.Nul }; 't' -> { i += 4; J.B(true) }; 'f' -> { i += 5; J.B(false) }
else -> J.Num(num())
} }
private fun str(): String { if (take() != '"') err(); val b = StringBuilder(); while (true) { when (val c = take()) {
'"' -> return b.toString()
'\\' -> when (val e = take()) {
'b' -> b.append('\b'); 'f' -> b.append('\u000C'); 'n' -> b.append('\n'); 'r' -> b.append('\r'); 't' -> b.append('\t')
'u' -> { var cp = 0; repeat(4) { cp = cp * 16 + take().digitToInt(16) }; b.append(cp.toChar()) }
else -> b.append(e) } // '"', '\\', '/' pass through
else -> b.append(c) } } }
private fun num(): String { val from = i
while ("-+.eE0123456789".contains(peek())) i++
if (i == from) err(); return s.substring(from, i) } // raw token, String(n)-safe
}
data class Res(val ok: Boolean, val sql: String, val rows: Int, val error: String?)
fun escapeSqlString(s: String, mysql: Boolean): String = buildString {
for (c in s) when {
c == '\'' -> append("''")
mysql && c == '\\' -> append("\\\\")
mysql && c == '\u0000' -> append("\\0") // NUL
mysql && c == '\n' -> append("\\n")
mysql && c == '\r' -> append("\\r")
mysql && c == '\u001a' -> append("\\Z") // SUB — ends input on some MySQL clients
else -> append(c) } }
fun sqlLiteral(v: J, mysql: Boolean): String = when (v) {
is J.Nul -> "NULL"
is J.B -> if (mysql) (if (v.v) "1" else "0") else (if (v.v) "TRUE" else "FALSE")
is J.Num -> v.raw; is J.S -> "'${escapeSqlString(v.s, mysql)}'"
is J.A, is J.O -> "'${escapeSqlString(compact(v), mysql)}'" } // object/array → JSON text
fun compact(v: J): String = when (v) { // JSON.stringify-style re-emission
is J.Nul -> "null"; is J.B -> v.v.toString(); is J.Num -> v.raw
is J.S -> "\"" + v.s.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + "\""
is J.A -> v.items.joinToString(",", "[", "]") { compact(it) }
is J.O -> v.pairs.entries.joinToString(",", "{", "}") { compact(J.S(it.key)) + ":" + compact(it.value) } }
fun quoteIdent(n: String, mysql: Boolean, quote: Boolean) = if (!quote) n else if (mysql) "`$n`" else "\"$n\""
fun sanitizeIdent(n: String): String = n.replace(Regex("[^A-Za-z0-9_]"), "_").ifEmpty { "tbl" }
/** Convert a JSON document into a single multi-row INSERT. A bare object is one
* row; an array is many. Heterogeneous rows unify via first-seen columns. */
fun jsonToInsert(json: String, table: String, mysql: Boolean = false, quote: Boolean = true): Res {
val root = try { Parser(json).parse() } catch (e: IllegalArgumentException) { return Res(false, "", 0, e.message) }
val rows: List<J> = if (root is J.A) root.items else listOf(root)
if (rows.isEmpty()) return Res(false, "", 0, "No rows to insert.")
if (rows.any { it !is J.O }) return Res(false, "", 0, "Rows must be objects.")
val cols = mutableListOf<String>() // first-seen union of every row's keys
for (r in rows) for (k in (r as J.O).pairs.keys) if (k !in cols) cols.add(k)
val tbl = quoteIdent(sanitizeIdent(table), mysql, quote)
val colList = cols.joinToString(", ") { quoteIdent(it, mysql, quote) }
val lines = rows.map { r -> val m = (r as J.O).pairs
" (" + cols.joinToString(", ") { sqlLiteral(m[it] ?: J.Nul, mysql) } + ")" }
return Res(true, "INSERT INTO $tbl ($colList) VALUES\n" + lines.joinToString(",\n") + ";", rows.size, null)
}
fun main() {
println(jsonToInsert("[{\"id\":1,\"name\":\"O'Hara\"},{\"id\":2,\"age\":30.5,\"tags\":[\"a\",\"b\"]}]", "users").sql)
// INSERT INTO "users" ("id", "name", "age", "tags") VALUES ...
}
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 →