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# port (canonical TS:
// src/lib/jsonToSql.ts; Go twin: cli/json-to-sql). Mirrors the JS/PHP ports:
// System.Text.Json preserves first-seen property order and Number raw text,
// so column unions and literals match the TS reference. Errors return through
// the Result record — never thrown.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;

public static class JsonToSql
{
    public sealed record Options(string Table, string Dialect = "standard", bool QuoteIdentifiers = true);
    public sealed record Result(bool Ok, string Sql, int Rows, string? Error);
    // Escape for a single-quoted SQL literal: '' doubling everywhere; MySQL
    // mode additionally escapes the backslash and control bytes that end input.
    static string EscapeSqlString(string s, string d) {
        var o = s.Replace("'", "''");
        if (d == "mysql") o = o.Replace("\\", "\\\\").Replace("\0", "\\0")
            .Replace("\n", "\\n").Replace("\r", "\\r").Replace("\x1a", "\\Z");
        return o;
    }
    // Render a decoded JSON value as a SQL literal.
    static string SqlLiteral(JsonElement v, string d) => v.ValueKind switch {
        JsonValueKind.Null => "NULL",
        JsonValueKind.True => d == "mysql" ? "1" : "TRUE",
        JsonValueKind.False => d == "mysql" ? "0" : "FALSE",
        JsonValueKind.Number => v.GetRawText(), // source token, String(n)-safe
        JsonValueKind.String => $"'{EscapeSqlString(v.GetString()!, d)}'",
        _ => $"'{EscapeSqlString(Compact(v), d)}'", // object/array → JSON text
    };
    // Compact JSON re-emission matching JSON.stringify: no whitespace, raw
    // non-ASCII and '/' (UnsafeRelaxedJsonEscaping is the JSON_UNESCAPED_* analogue).
    static string Compact(JsonElement v) {
        using var ms = new MemoryStream();
        using (var w = new Utf8JsonWriter(ms, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }))
            v.WriteTo(w);
        return Encoding.UTF8.GetString(ms.ToArray());
    }
    // Quote per dialect, or leave bare when quoting is disabled. Sanitize first:
    // [^A-Za-z0-9_] -> '_', defaulting to "tbl" when nothing usable remains.
    static string QuoteIdent(string n, string d, bool q) => !q ? n : d == "mysql" ? $"`{n}`" : $"\"{n}\"";
    static string SanitizeIdent(string name) {
        var cleaned = new string(Array.ConvertAll(name.ToCharArray(),
            c => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (>= '0' and <= '9') or '_' ? c : '_'));
        return cleaned.Length == 0 ? "tbl" : cleaned;
    }
    // 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.
    public static Result JsonToInsert(string json, Options opts) {
        JsonDocument doc;
        try { doc = JsonDocument.Parse(json); } catch (JsonException e) { return new Result(false, "", 0, e.Message); }
        using (doc) {
            var root = doc.RootElement;
            var rows = root.ValueKind == JsonValueKind.Array ? root.EnumerateArray().ToList()
                     : new List<JsonElement> { root };
            if (rows.Count == 0) return new Result(false, "", 0, "No rows to insert.");
            if (rows.Any(r => r.ValueKind != JsonValueKind.Object)) return new Result(false, "", 0, "Rows must be objects.");
            var d = opts.Dialect; var q = opts.QuoteIdentifiers;
            var table = QuoteIdent(SanitizeIdent(opts.Table), d, q);
            var cols = new List<string>(); // first-seen union of every row's keys
            foreach (var r in rows) foreach (var p in r.EnumerateObject()) if (!cols.Contains(p.Name)) cols.Add(p.Name);
            var colList = string.Join(", ", cols.Select(c => QuoteIdent(c, d, q)));
            var lines = rows.Select(r => $"  ({string.Join(", ", cols.Select(c => r.TryGetProperty(c, out var v) ? SqlLiteral(v, d) : "NULL"))})");
            return new Result(true, $"INSERT INTO {table} ({colList}) VALUES\n{string.Join(",\n", lines)};", rows.Count, null);
        }
    }
    public static void Main() {
        var r = JsonToInsert("[{\"id\":1,\"name\":\"O'Hara\"},{\"id\":2,\"age\":30.5,\"tags\":[\"a\",\"b\"]}]", new Options("users"));
        Console.WriteLine(r.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 →