Skip to content

JSON ↔ CSV Converter — C source

Convert a JSON array of objects to CSV and back. Handles quoted fields, embedded commas, newlines and escaped quotes (RFC 4180). 100% in-browser.

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

/* =============================================================================
 * json-csv — C port
 * =============================================================================
 * Convert between JSON and RFC 4180 CSV in either direction:
 *   • json_to_csv — serialize a JSON document (object or array of objects) to CSV
 *   • csv_to_json — parse RFC 4180 CSV (with quoting) into a list of row records
 *
 * Language: C11 — standard library only; minimal embedded JSON parser.
 * Source: CosmoDev polyglot showcase port of json-csv,
 *         ported from src/lib/csv.ts (the canonical, live TypeScript lib).
 * License: display source — part of CosmoDev's polyglot tool pages.
 *
 * Pure and deterministic — depends only on its inputs. RFC 4180 quoting: any
 * field containing a comma, double quote, carriage return, or line feed is
 * wrapped in double quotes, and each embedded quote is doubled.
 *
 * This is display source — part of CosmoDev's polyglot tool pages.
 * =============================================================================
 */

/* C's stdlib has no JSON support, so this file is self-contained: a small
 * recursive-descent parser, mirroring the Rust sibling snippet (which would
 * use an ecosystem crate such as `serde_json` instead). Object pairs live in
 * an array so key order — which is observable (it determines CSV column
 * order) — is preserved, exactly as in the canonical lib.
 *
 * Memory discipline: every successful call returns heap allocations owned by
 * the caller; json_value_free / csv_rows_free release them. json_to_csv
 * returns NULL on invalid JSON or when the document yields no headers (the
 * Python sibling likewise collapses both cases to None). */

#include <setjmp.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ---------------------------------------------------------------------------
 * JSON value tree
 * --------------------------------------------------------------------------- */

typedef enum { J_NULL, J_BOOL, J_NUMBER, J_STR, J_ARRAY, J_OBJECT } JKind;

typedef struct JValue JValue;

void json_value_free(JValue *v); /* recursive; declared early for j_pair */

typedef struct {
    char *key;   /* owned */
    JValue *val; /* owned */
} JPair;

struct JValue {
    JKind kind;
    bool b;
    double num;
    char *str;      /* owned; J_STR only */
    JValue **items; /* owned array of owned; J_ARRAY only */
    size_t n_items;
    JPair *pairs;   /* owned array; J_OBJECT only, insertion-ordered */
    size_t n_pairs;
};

static JValue *j_null(void) {
    JValue *v = calloc(1, sizeof(JValue));
    if (!v) { fprintf(stderr, "out of memory\n"); exit(1); }
    v->kind = J_NULL;
    return v;
}

static JValue *j_bool(bool b) {
    JValue *v = j_null();
    v->kind = J_BOOL;
    v->b = b;
    return v;
}

static JValue *j_number(double n) {
    JValue *v = j_null();
    v->kind = J_NUMBER;
    v->num = n;
    return v;
}

static JValue *j_str_take(char *s) {
    JValue *v = j_null();
    v->kind = J_STR;
    v->str = s;
    return v;
}

static void j_push(JValue *arr, JValue *item) {
    JValue **grown = realloc(arr->items, (arr->n_items + 1) * sizeof(JValue *));
    if (!grown) { fprintf(stderr, "out of memory\n"); exit(1); }
    arr->items = grown;
    arr->items[arr->n_items++] = item;
}

static void j_pair(JValue *obj, char *key, JValue *val) {
    /* First occurrence of a key wins, matching JS object-literal semantics. */
    for (size_t i = 0; i < obj->n_pairs; i++) {
        if (strcmp(obj->pairs[i].key, key) == 0) {
            free(key);
            json_value_free(val);
            return;
        }
    }
    JPair *grown = realloc(obj->pairs, (obj->n_pairs + 1) * sizeof(JPair));
    if (!grown) { fprintf(stderr, "out of memory\n"); exit(1); }
    obj->pairs = grown;
    obj->pairs[obj->n_pairs].key = key;
    obj->pairs[obj->n_pairs].val = val;
    obj->n_pairs++;
}

void json_value_free(JValue *v) {
    if (!v) return;
    free(v->str);
    for (size_t i = 0; i < v->n_items; i++) json_value_free(v->items[i]);
    free(v->items);
    for (size_t i = 0; i < v->n_pairs; i++) {
        free(v->pairs[i].key);
        json_value_free(v->pairs[i].val);
    }
    free(v->pairs);
    free(v);
}

/* ---------------------------------------------------------------------------
 * Minimal JSON parser
 * ---------------------------------------------------------------------------
 * Compact recursive-descent parser. Sufficient for any RFC 8259 document a
 * caller is likely to feed this tool. */

typedef struct {
    const char *s;
    size_t pos, len;
} Parser;

/* Errors unwind via longjmp back to parse_json, which returns NULL — the
 * C equivalent of the sibling ports' Err/None. Deep-abort partial
 * allocations are reclaimed at process exit (showcase scope; the canonical
 * TS lib collects via GC). */
static jmp_buf g_err_jmp;

static void die(const char *msg) {
    (void)msg;
    longjmp(g_err_jmp, 1);
}

static void skip_ws(Parser *p) {
    while (p->pos < p->len) {
        char c = p->s[p->pos];
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r') p->pos++;
        else break;
    }
}

static char peek(Parser *p) {
    if (p->pos >= p->len) die("unexpected end of input");
    return p->s[p->pos];
}

static JValue *parse_value(Parser *p);

static char *parse_string_raw(Parser *p) {
    p->pos++; /* opening quote */
    size_t cap = 16, n = 0;
    char *out = malloc(cap);
    if (!out) { fprintf(stderr, "out of memory\n"); exit(1); }
    while (p->pos < p->len) {
        char c = p->s[p->pos++];
        if (c == '"') {
            out[n] = '\0';
            return out;
        }
        if (c == '\\') {
            if (p->pos >= p->len) die("trailing escape");
            char e = p->s[p->pos++];
            long cp = -1;
            switch (e) {
                case '"':  cp = '"';  break;
                case '\\': cp = '\\'; break;
                case '/':  cp = '/';  break;
                case 'n':  cp = '\n'; break;
                case 't':  cp = '\t'; break;
                case 'r':  cp = '\r'; break;
                case 'b':  cp = '\b'; break;
                case 'f':  cp = '\f'; break;
                case 'u': {
                    if (p->pos + 4 > p->len) die("short \\u escape");
                    cp = 0;
                    for (int i = 0; i < 4; i++) {
                        char h = p->s[p->pos++];
                        cp <<= 4;
                        if (h >= '0' && h <= '9') cp += h - '0';
                        else if (h >= 'a' && h <= 'f') cp += h - 'a' + 10;
                        else if (h >= 'A' && h <= 'F') cp += h - 'A' + 10;
                        else die("bad \\u escape");
                    }
                    /* UTF-16 surrogate pair handling. */
                    if (cp >= 0xD800 && cp <= 0xDBFF
                            && p->pos + 6 <= p->len
                            && p->s[p->pos] == '\\' && p->s[p->pos + 1] == 'u') {
                        unsigned lo = 0;
                        bool ok = true;
                        for (int i = 0; i < 4; i++) {
                            char h = p->s[p->pos + 2 + i];
                            lo <<= 4;
                            if (h >= '0' && h <= '9') lo += (unsigned)(h - '0');
                            else if (h >= 'a' && h <= 'f') lo += (unsigned)(h - 'a' + 10);
                            else if (h >= 'A' && h <= 'F') lo += (unsigned)(h - 'A' + 10);
                            else { ok = false; break; }
                        }
                        if (ok && lo >= 0xDC00 && lo <= 0xDFFF) {
                            p->pos += 6;
                            cp = 0x10000 + ((cp - 0xD800) << 10) + (long)(lo - 0xDC00);
                        }
                    }
                    /* fallthrough: cp appended below */
                    break;
                }
                default: die("bad escape");
            }
            /* Encode one codepoint as UTF-8. */
            if (cp < 0x80) {
                out[n++] = (char)cp;
            } else if (cp < 0x800) {
                out[n++] = (char)(0xC0 | (cp >> 6));
                out[n++] = (char)(0x80 | (cp & 0x3F));
            } else if (cp < 0x10000) {
                out[n++] = (char)(0xE0 | (cp >> 12));
                out[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
                out[n++] = (char)(0x80 | (cp & 0x3F));
            } else {
                out[n++] = (char)(0xF0 | (cp >> 18));
                out[n++] = (char)(0x80 | ((cp >> 12) & 0x3F));
                out[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
                out[n++] = (char)(0x80 | (cp & 0x3F));
            }
        } else {
            out[n++] = c;
        }
        if (n + 5 >= cap) {
            cap *= 2;
            char *grown = realloc(out, cap);
            if (!grown) { fprintf(stderr, "out of memory\n"); exit(1); }
            out = grown;
        }
    }
    die("unterminated string");
    return NULL; /* unreachable */
}

static JValue *parse_object(Parser *p) {
    p->pos++; /* { */
    JValue *obj = j_null();
    obj->kind = J_OBJECT;
    skip_ws(p);
    if (peek(p) == '}') {
        p->pos++;
        return obj;
    }
    for (;;) {
        skip_ws(p);
        if (peek(p) != '"') die("expected string key in object");
        char *key = parse_string_raw(p);
        skip_ws(p);
        if (peek(p) != ':') die("expected ':' after object key");
        p->pos++;
        JValue *val = parse_value(p);
        j_pair(obj, key, val);
        skip_ws(p);
        char c = peek(p);
        if (c == ',') { p->pos++; continue; }
        if (c == '}') { p->pos++; return obj; }
        die("expected ',' or '}' in object");
    }
}

static JValue *parse_array(Parser *p) {
    p->pos++; /* [ */
    JValue *arr = j_null();
    arr->kind = J_ARRAY;
    skip_ws(p);
    if (peek(p) == ']') {
        p->pos++;
        return arr;
    }
    for (;;) {
        j_push(arr, parse_value(p));
        skip_ws(p);
        char c = peek(p);
        if (c == ',') { p->pos++; continue; }
        if (c == ']') { p->pos++; return arr; }
        die("expected ',' or ']' in array");
    }
}

static JValue *parse_number(Parser *p) {
    size_t start = p->pos;
    if (p->pos < p->len && p->s[p->pos] == '-') p->pos++;
    while (p->pos < p->len) {
        char c = p->s[p->pos];
        if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') {
            p->pos++;
        } else {
            break;
        }
    }
    char buf[64];
    size_t n = p->pos - start;
    if (n >= sizeof(buf)) die("bad number");
    memcpy(buf, p->s + start, n);
    buf[n] = '\0';
    char *end = NULL;
    double d = strtod(buf, &end);
    if (end == buf || *end != '\0') die("bad number");
    return j_number(d);
}

static JValue *parse_value(Parser *p) {
    skip_ws(p);
    char c = peek(p);
    switch (c) {
        case '{': return parse_object(p);
        case '[': return parse_array(p);
        case '"': return j_str_take(parse_string_raw(p));
        case 't':
            if (p->len - p->pos >= 4 && memcmp(p->s + p->pos, "true", 4) == 0) {
                p->pos += 4;
                return j_bool(true);
            }
            die("invalid literal");
        case 'f':
            if (p->len - p->pos >= 5 && memcmp(p->s + p->pos, "false", 5) == 0) {
                p->pos += 5;
                return j_bool(false);
            }
            die("invalid literal");
        case 'n':
            if (p->len - p->pos >= 4 && memcmp(p->s + p->pos, "null", 4) == 0) {
                p->pos += 4;
                return j_null();
            }
            die("invalid literal");
        default:
            if (c == '-' || (c >= '0' && c <= '9')) return parse_number(p);
            die("unexpected character");
    }
    return NULL; /* unreachable */
}

/* Parse a JSON document into a JValue. Returns NULL on malformed input. */
static JValue *parse_json(const char *input) {
    Parser p = { input, 0, strlen(input) };
    if (setjmp(g_err_jmp) != 0) return NULL;
    JValue *v = parse_value(&p);
    skip_ws(&p);
    if (p.pos != p.len) die("trailing data");
    return v;
}

/* ---------------------------------------------------------------------------
 * Growable output buffer
 * --------------------------------------------------------------------------- */

typedef struct {
    char *data;
    size_t len, cap;
} Buf;

static void buf_init(Buf *b) {
    b->cap = 64;
    b->len = 0;
    b->data = malloc(b->cap);
    if (!b->data) { fprintf(stderr, "out of memory\n"); exit(1); }
    b->data[0] = '\0';
}

static void buf_grow(Buf *b, size_t need) {
    if (b->len + need + 1 <= b->cap) return;
    while (b->cap < b->len + need + 1) b->cap *= 2;
    char *grown = realloc(b->data, b->cap);
    if (!grown) { fprintf(stderr, "out of memory\n"); exit(1); }
    b->data = grown;
}

static void buf_ch(Buf *b, char c) {
    buf_grow(b, 1);
    b->data[b->len++] = c;
    b->data[b->len] = '\0';
}

static void buf_str(Buf *b, const char *s) {
    size_t n = strlen(s);
    buf_grow(b, n);
    memcpy(b->data + b->len, s, n);
    b->len += n;
    b->data[b->len] = '\0';
}

/* ---------------------------------------------------------------------------
 * JS-equivalent value semantics
 * ---------------------------------------------------------------------------
 * The canonical lib uses `typeof x === 'object'` and Object.keys(x), which in
 * JavaScript treat BOTH objects and arrays as "object" and expose array
 * indices as string keys ("0", "1", ...). We mirror that so degenerate inputs
 * (e.g. an array of arrays) produce byte-identical output to the TS. */

/* Render a number the way JS String(number) does on common inputs: shortest
 * decimal form that round-trips, with integral doubles printed without a
 * trailing ".0" (e.g. `30.0` -> "30"). */
static void format_number(Buf *out, double f) {
    if (f == (double)(long long)f && (f < 0 ? -f : f) < 1e16) {
        char tmp[32];
        snprintf(tmp, sizeof(tmp), "%lld", (long long)f);
        buf_str(out, tmp);
        return;
    }
    char tmp[40];
    for (int prec = 1; prec <= 17; prec++) {
        snprintf(tmp, sizeof(tmp), "%.*g", prec, f);
        if (strtod(tmp, NULL) == f) break;
    }
    buf_str(out, tmp);
}

/* Coerce a JSON value to its display string, replicating JavaScript's
 * String(): null -> "", booleans -> "true"/"false", numbers -> decimal form,
 * arrays -> elements joined by "," (so a comma-bearing cell re-quotes), and
 * objects -> "[object Object]". */
static void js_string(Buf *out, const JValue *v) {
    switch (v->kind) {
        case J_NULL: return;
        case J_BOOL: buf_str(out, v->b ? "true" : "false"); return;
        case J_NUMBER: format_number(out, v->num); return;
        case J_STR: buf_str(out, v->str); return;
        case J_ARRAY:
            for (size_t i = 0; i < v->n_items; i++) {
                if (i > 0) buf_ch(out, ',');
                js_string(out, v->items[i]);
            }
            return;
        case J_OBJECT: buf_str(out, "[object Object]"); return;
    }
}

/* Quote a single CSV field per RFC 4180. */
static void csv_escape(Buf *out, const JValue *v) {
    Buf s;
    buf_init(&s);
    js_string(&s, v);
    bool quote = false;
    for (size_t i = 0; i < s.len; i++) {
        char c = s.data[i];
        if (c == ',' || c == '"' || c == '\n' || c == '\r') { quote = true; break; }
    }
    if (!quote) {
        buf_str(out, s.data);
        free(s.data);
        return;
    }
    buf_ch(out, '"');
    for (size_t i = 0; i < s.len; i++) {
        if (s.data[i] == '"') buf_ch(out, '"');
        buf_ch(out, s.data[i]);
    }
    buf_ch(out, '"');
    free(s.data);
}

/* Object.keys parity: array indices as strings ("0", "1", ...), or the
 * object's insertion-ordered keys. Primitives and null yield no keys. */
static size_t keys_of(const JValue *v, char ***keys_out) {
    size_t n = 0;
    if (v->kind == J_ARRAY) n = v->n_items;
    else if (v->kind == J_OBJECT) n = v->n_pairs;
    else { *keys_out = NULL; return 0; }
    char **keys = malloc(n * sizeof(char *));
    if (!keys) { fprintf(stderr, "out of memory\n"); exit(1); }
    if (v->kind == J_ARRAY) {
        for (size_t i = 0; i < n; i++) {
            char tmp[24];
            snprintf(tmp, sizeof(tmp), "%zu", i);
            keys[i] = strdup(tmp);
        }
    } else {
        for (size_t i = 0; i < n; i++) keys[i] = strdup(v->pairs[i].key);
    }
    *keys_out = keys;
    return n;
}

static void free_keys(char **keys, size_t n) {
    for (size_t i = 0; i < n; i++) free(keys[i]);
    free(keys);
}

/* JS `obj[key]` parity: object lookup, or array element at a non-negative
 * integer index. Returns NULL when absent (which renders as the empty field). */
static const JValue *get_field(const JValue *v, const char *key) {
    if (v->kind == J_OBJECT) {
        for (size_t i = 0; i < v->n_pairs; i++) {
            if (strcmp(v->pairs[i].key, key) == 0) return v->pairs[i].val;
        }
        return NULL;
    }
    if (v->kind == J_ARRAY) {
        /* only pure decimal digits index an array; reject huge values */
        if (key[0] == '\0' || strlen(key) > 18) return NULL;
        for (const char *c = key; *c; c++) {
            if (*c < '0' || *c > '9') return NULL;
        }
        unsigned long long i = strtoull(key, NULL, 10);
        return i < v->n_items ? v->items[i] : NULL;
    }
    return NULL;
}

/* ---------------------------------------------------------------------------
 * Public API
 * --------------------------------------------------------------------------- */

/* One deserialized CSV record: an ordered array of (header, cell) pairs. We
 * use a pair array rather than a hash map so duplicate/empty headers survive
 * round-trips, exactly as in the TS lib's `Record<string, string>` indexing. */
typedef struct {
    char **headers; /* owned array of owned */
    char **cells;   /* owned array of owned */
    size_t n;
} CsvRow;

void csv_rows_free(CsvRow *rows, size_t n_rows) {
    for (size_t r = 0; r < n_rows; r++) {
        for (size_t i = 0; i < rows[r].n; i++) {
            free(rows[r].headers[i]);
            free(rows[r].cells[i]);
        }
        free(rows[r].headers);
        free(rows[r].cells);
    }
    free(rows);
}

/* Serialize a JSON document to CSV. Returns a malloc'd string, or NULL on
 * invalid input or when the document yields no object rows (and thus no
 * column headers) — e.g. a bare array of primitives such as `[1, 2, 3]`.
 * Accepts a single object or an array of objects. */
char *json_to_csv(const char *text) {
    JValue *data = parse_json(text);
    if (!data) return NULL;

    /* A bare value is treated as a one-row table. */
    const JValue **rows;
    size_t n_rows;
    if (data->kind == J_ARRAY) {
        n_rows = data->n_items;
        rows = malloc((n_rows ? n_rows : 1) * sizeof(JValue *));
        if (!rows) { fprintf(stderr, "out of memory\n"); exit(1); }
        for (size_t i = 0; i < n_rows; i++) rows[i] = data->items[i];
    } else {
        n_rows = 1;
        rows = malloc(sizeof(JValue *));
        if (!rows) { fprintf(stderr, "out of memory\n"); exit(1); }
        rows[0] = data;
    }

    /* Header union across object-like rows, first-seen order, de-duplicated. */
    char **headers = NULL;
    size_t n_headers = 0;
    for (size_t r = 0; r < n_rows; r++) {
        char **keys = NULL;
        size_t n_keys = keys_of(rows[r], &keys);
        for (size_t k = 0; k < n_keys; k++) {
            bool dup = false;
            for (size_t h = 0; h < n_headers; h++) {
                if (strcmp(headers[h], keys[k]) == 0) { dup = true; break; }
            }
            if (!dup) {
                char **grown = realloc(headers, (n_headers + 1) * sizeof(char *));
                if (!grown) { fprintf(stderr, "out of memory\n"); exit(1); }
                headers = grown;
                headers[n_headers++] = strdup(keys[k]);
            }
        }
        free_keys(keys, n_keys);
    }
    if (n_headers == 0) {
        free(rows);
        json_value_free(data);
        return NULL;
    }

    /* First line is the (escaped) header row; subsequent lines are the rows. */
    Buf out;
    buf_init(&out);
    for (size_t h = 0; h < n_headers; h++) {
        if (h > 0) buf_ch(&out, ',');
        JValue hv = { .kind = J_STR, .str = headers[h] };
        csv_escape(&out, &hv);
    }
    for (size_t r = 0; r < n_rows; r++) {
        buf_ch(&out, '\n');
        /* A non-object row (null, number, string) yields an empty line: every
         * header lookup on it returns NULL -> the empty field. */
        for (size_t h = 0; h < n_headers; h++) {
            if (h > 0) buf_ch(&out, ',');
            const JValue *cell = get_field(rows[r], headers[h]);
            if (cell) {
                csv_escape(&out, cell);
            } else {
                JValue nullv = { .kind = J_NULL };
                csv_escape(&out, &nullv);
            }
        }
    }

    free_keys(headers, n_headers);
    free(rows);
    json_value_free(data);
    return out.data;
}

/* Parse RFC 4180 CSV into an array of rows keyed by the first row.
 *
 * Handles quoted fields, doubled-quote escapes, and embedded
 * commas/newlines; bare carriage returns outside quotes are ignored. Sets
 * *n_rows_out to zero for empty input, or for input that is only a header
 * row. Caller owns the result via csv_rows_free. */
CsvRow *csv_to_json(const char *text, size_t *n_rows_out) {
    /* Single-pass character-state machine over bytes. Field content outside
     * ASCII passes through untouched (UTF-8 is transparent to the machine). */
    CsvRow *rows = NULL;
    size_t n_rows = 0;
    Buf field;
    buf_init(&field);
    char **row = NULL;
    size_t n_cells = 0;
    bool in_quotes = false;

    size_t len = strlen(text);
    for (size_t i = 0; i < len; i++) {
        char ch = text[i];
        if (in_quotes) {
            if (ch == '"') {
                /* Doubled quote -> one literal quote; lone quote -> close field. */
                if (i + 1 < len && text[i + 1] == '"') {
                    buf_ch(&field, '"');
                    i++;
                    continue;
                }
                in_quotes = false;
            } else {
                buf_ch(&field, ch);
            }
        } else if (ch == '"') {
            in_quotes = true;
        } else if (ch == ',') {
            row = realloc(row, (n_cells + 1) * sizeof(char *));
            if (!row) { fprintf(stderr, "out of memory\n"); exit(1); }
            row[n_cells++] = strdup(field.data);
            field.len = 0;
            field.data[0] = '\0';
        } else if (ch == '\n') {
            row = realloc(row, (n_cells + 1) * sizeof(char *));
            if (!row) { fprintf(stderr, "out of memory\n"); exit(1); }
            row[n_cells++] = strdup(field.data);
            field.len = 0;
            field.data[0] = '\0';
            rows = realloc(rows, (n_rows + 1) * sizeof(CsvRow));
            if (!rows) { fprintf(stderr, "out of memory\n"); exit(1); }
            rows[n_rows].headers = row;
            rows[n_rows].cells = row; /* cells stored in headers slot until keyed */
            rows[n_rows].n = n_cells;
            n_rows++;
            row = NULL;
            n_cells = 0;
        } else if (ch != '\r') {
            buf_ch(&field, ch);
        }
    }
    /* Flush a trailing row only when there is pending content. Input that
     * ended with a newline already flushed; this guard avoids an empty final
     * row. */
    if (field.len > 0 || n_cells > 0) {
        row = realloc(row, (n_cells + 1) * sizeof(char *));
        if (!row) { fprintf(stderr, "out of memory\n"); exit(1); }
        row[n_cells++] = strdup(field.data);
        rows = realloc(rows, (n_rows + 1) * sizeof(CsvRow));
        if (!rows) { fprintf(stderr, "out of memory\n"); exit(1); }
        rows[n_rows].headers = row;
        rows[n_rows].cells = row;
        rows[n_rows].n = n_cells;
        n_rows++;
    }
    free(field.data);

    if (n_rows == 0) {
        *n_rows_out = 0;
        return NULL;
    }

    /* Key rows 1..n by the header row: each output row pairs row 0's header
     * text with its own cells. */
    CsvRow *out = NULL;
    size_t n_out = 0;
    for (size_t r = 1; r < n_rows; r++) {
        CsvRow *src = &rows[r];
        size_t n = rows[0].n;
        char **hs = malloc(n * sizeof(char *));
        char **cs = malloc(n * sizeof(char *));
        if (!hs || !cs) { fprintf(stderr, "out of memory\n"); exit(1); }
        for (size_t i = 0; i < n; i++) {
            hs[i] = strdup(rows[0].headers[i]); /* header text (row 0 cells) */
            cs[i] = strdup(i < src->n ? src->cells[i] : "");
        }
        out = realloc(out, (n_out + 1) * sizeof(CsvRow));
        if (!out) { fprintf(stderr, "out of memory\n"); exit(1); }
        out[n_out].headers = hs;
        out[n_out].cells = cs;
        out[n_out].n = n;
        n_out++;
    }

    /* Free the raw rows (each held one flat array in both slots; free once). */
    for (size_t r = 0; r < n_rows; r++) {
        for (size_t i = 0; i < rows[r].n; i++) free(rows[r].headers[i]);
        free(rows[r].headers);
    }
    free(rows);

    *n_rows_out = n_out;
    return out;
}

int main(void) {
    /* Small end-to-end demo so this file is runnable as a showcase. */
    const char *raw =
        "[{\"name\":\"Doe, John\",\"note\":\"say \\\"hi\\\"\"},{\"name\":\"Jane\",\"note\":\"plain\"}]";
    char *csv = json_to_csv(raw);
    if (!csv) {
        fprintf(stderr, "error: no CSV produced\n");
        return 1;
    }
    printf("%s\n", csv);

    size_t n_rows = 0;
    CsvRow *rows = csv_to_json(csv, &n_rows);
    for (size_t r = 0; r < n_rows; r++) {
        printf("{");
        for (size_t i = 0; i < rows[r].n; i++) {
            printf("%s%s=\"%s\"", i > 0 ? ", " : "", rows[r].headers[i], rows[r].cells[i]);
        }
        printf("}\n");
    }
    csv_rows_free(rows, n_rows);
    free(csv);
    return 0;
}

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 →