Skip to content

JSON to SQL INSERT — C++ 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 C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// json-to-sql — pure JSON → SQL INSERT converter. C++17 port (canonical TS:
// src/lib/jsonToSql.ts; Go twin: cli/json-to-sql). No stdlib JSON codec: a
// hand-rolled JSON DOM parser (vector-of-pairs objects keep first-seen key
// order; number tokens stay raw text) feeding a multi-row INSERT builder.
// Errors return through Res — never thrown on user input.
#include <algorithm>
#include <cstring>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using std::string; using std::vector;
struct V {
    enum Tag { NUL, T, F, NUM, STR, OBJ, ARR } tag = NUL;
    string s;                            // NUM raw token ("String(n)"-safe) or STR value
    vector<std::pair<string, V>> ps;     // OBJ: insertion-ordered pairs
    vector<V> items;                     // ARR
};
struct Res { bool ok; string sql; int rows; string err; };
struct Parser {
    struct Bad {};
    string s; size_t i = 0;
    explicit Parser(string in) : s(std::move(in)) {}
    [[noreturn]] static void err() { throw Bad{}; }
    static bool wschar(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
    char peek() const { return i < s.size() ? s[i] : '\0'; }
    char take() { if (i >= s.size()) err(); return s[i++]; }
    void ws() { while (wschar(peek())) ++i; }
    V value() {
        ws(); char c = peek();
        if (c == '{') { ++i; ws(); V v; v.tag = V::OBJ;
            if (peek() == '}') { ++i; return v; }
            while (true) { ws(); string k = str(); ws(); if (take() != ':') err();
                v.ps.emplace_back(std::move(k), value()); ws();
                char d = take(); if (d == '}') return v; if (d != ',') err(); } }
        if (c == '[') { ++i; ws(); V v; v.tag = V::ARR;
            if (peek() == ']') { ++i; return v; }
            while (true) { v.items.push_back(value()); ws();
                char d = take(); if (d == ']') return v; if (d != ',') err(); } }
        if (c == '"') { V v; v.tag = V::STR; v.s = str(); return v; }
        if (!s.compare(i, 4, "null"))  { i += 4; return V{}; }
        if (!s.compare(i, 4, "true"))  { i += 4; V v; v.tag = V::T; return v; }
        if (!s.compare(i, 5, "false")) { i += 5; V v; v.tag = V::F; return v; }
        size_t from = i;
        while (peek() && strchr("-+.eE0123456789", peek())) ++i;
        if (i == from) err();
        V v; v.tag = V::NUM; v.s = s.substr(from, i - from); return v; // raw token
    }
    string str() {
        if (take() != '"') err();
        string o;
        while (true) { char c = take();
            if (c == '"') return o;
            if (c == '\\') { char e = take();
                if (e == 'u') { int cp = 0; for (int k = 0; k < 4; ++k) { char h = take();
                        cp = cp * 16 + (h <= '9' ? h - '0' : (h | 32) - 'a' + 10); } o += (char)cp; }
                else o += e == 'n' ? '\n' : e == 'r' ? '\r' : e == 't' ? '\t'
                      : e == 'b' ? '\b' : e == 'f' ? '\f' : e; } // '"', '\\', '/' pass through
            else o += c; }
    }
};
string escapeSqlString(const string& s, bool mysql) {
    string o;
    for (char c : s) {
        if (c == '\'') o += "''";
        else if (mysql && c == '\\') o += "\\\\";
        else if (mysql && c == '\n') o += "\\n";
        else if (mysql && c == '\r') o += "\\r";
        else if (mysql && c == '\x1a') o += "\\Z"; // SUB — ends MySQL input
        else o += c; // (NUL cannot occur in a C string)
    }
    return o;
}
string compact(const V& v) { // JSON.stringify-style re-emission
    switch (v.tag) {
        case V::NUL: return "null"; case V::T: return "true"; case V::F: return "false"; case V::NUM: return v.s;
        case V::STR: { string o = "\"";
            for (char c : v.s) { if (c == '"' || c == '\\') { o += '\\'; o += c; }
                else if (c == '\n') o += "\\n"; else if (c == '\r') o += "\\r";
                else if (c == '\t') o += "\\t"; else o += c; }
            return o + "\""; }
        case V::ARR: { string o = "["; bool first = true;
            for (const auto& x : v.items) { if (!first) o += ','; first = false; o += compact(x); }
            return o + "]"; }
        case V::OBJ: { string o = "{"; bool first = true;
            for (const auto& [k, x] : v.ps) { if (!first) o += ','; first = false;
                o += compact(V{V::STR, k, {}, {}}) + ':' + compact(x); }
            return o + "}"; }
    }
    return "";
}
string sqlLiteral(const V& v, bool mysql) {
    switch (v.tag) {
        case V::NUL: return "NULL";
        case V::T: return mysql ? "1" : "TRUE";
        case V::F: return mysql ? "0" : "FALSE";
        case V::NUM: return v.s;
        case V::STR: return "'" + escapeSqlString(v.s, mysql) + "'";
        default: return "'" + escapeSqlString(compact(v), mysql) + "'"; // OBJ/ARR → JSON text
    }
}
string ident(string n, bool mysql, bool quote) { // [^A-Za-z0-9_] -> '_', default "tbl"
    for (char& c : n) { bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
        if (!ok) c = '_'; }
    if (n.empty()) n = "tbl";
    if (!quote) return n;
    return mysql ? "`" + n + "`" : "\"" + n + "\"";
}
// 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.
Res jsonToInsert(const string& json, const string& table, bool mysql = false, bool quote = true) {
    try {
        V root = Parser(json).value();
        vector<const V*> rows;
        if (root.tag == V::ARR) for (const auto& x : root.items) rows.push_back(&x);
        else rows.push_back(&root);
        if (rows.empty()) return {false, "", 0, "No rows to insert."};
        for (const V* r : rows) if (r->tag != V::OBJ) return {false, "", 0, "Rows must be objects."};
        vector<string> cols; // first-seen union of every row's keys
        for (const V* r : rows) for (const auto& [k, x] : r->ps)
            if (std::find(cols.begin(), cols.end(), k) == cols.end()) cols.push_back(k);
        string sql = "INSERT INTO " + ident(table, mysql, quote) + " (";
        for (size_t c = 0; c < cols.size(); ++c) { if (c) sql += ", "; sql += ident(cols[c], mysql, quote); }
        sql += ") VALUES\n";
        for (size_t r = 0; r < rows.size(); ++r) {
            sql += "  (";
            for (size_t c = 0; c < cols.size(); ++c) {
                if (c) sql += ", ";
                const V* cell = nullptr; // absent column -> NULL
                for (const auto& [k, x] : rows[r]->ps) if (k == cols[c]) cell = &x;
                sql += cell ? sqlLiteral(*cell, mysql) : "NULL";
            }
            sql += r + 1 == rows.size() ? ");" : "),\n";
        }
        return {true, sql, (int)rows.size(), ""};
    } catch (const Parser::Bad&) { return {false, "", 0, "Invalid JSON"}; }
}
int main() {
    std::cout << jsonToInsert("[{\"id\":1,\"name\":\"O'Hara\"},{\"id\":2,\"age\":30.5,\"tags\":[\"a\",\"b\"]}]", "users").sql << "\n";
    // 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 →