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 →