JSON Formatter — C source
Beautify, minify and validate JSON instantly. Paste, format, copy - all in your browser. Share a link to your exact input.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* json-formatter — parse, pretty-print (2-space indent), and minify.
*
* Language: C (C11, standard library only — C ships no JSON facility)
* Source: CosmoDev polyglot showcase port of the `json-formatter` tool,
* ported from src/tools/JsonFormatter.tsx (the canonical
* TypeScript island).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Mirroring the Rust port, this file includes a small hand-written
* recursive-descent parser and serializer. Object members are stored in a
* dynamically grown key/value array, which preserves source insertion order
* just like a JavaScript engine.
*
* Parity caveats: numbers are held as double (matching JS Number), so
* integers beyond 2^53 lose precision, and printf %g switches to exponential
* notation at a different threshold than ECMAScript's Number::toString
* (e.g. 1e15 prints as "1e+15"). Non-ASCII bytes pass through untouched —
* the input is assumed to be UTF-8.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* Public surface — mirrors the TypeScript { output, error } shape. */
/* ------------------------------------------------------------------ */
typedef enum { MODE_BEAUTIFY, MODE_MINIFY } Mode;
typedef struct {
char *output; /* heap string (caller frees); "" on empty input or error */
char *error; /* heap string (caller frees); NULL on success */
} FormatResult;
typedef enum {
JSON_NULL,
JSON_BOOL,
JSON_NUMBER,
JSON_STRING,
JSON_ARRAY,
JSON_OBJECT
} JsonType;
typedef struct JsonValue JsonValue;
typedef struct {
char *key;
JsonValue *value;
} JsonMember;
typedef struct {
JsonMember *items;
size_t len, cap;
} JsonObject;
typedef struct {
JsonValue **items;
size_t len, cap;
} JsonArray;
struct JsonValue {
JsonType type;
union {
bool boolean;
double number;
char *string; /* NUL-terminated UTF-8 */
JsonArray array;
JsonObject object;
} as;
};
/* Parse and re-emit a JSON document the way the live island does. `indent`
* is the number of spaces per nesting level for MODE_BEAUTIFY (pass 2 to
* match the UI). Empty/whitespace-only input yields empty output, no error. */
static FormatResult format_json(const char *input, Mode mode, int indent);
/* Free a value tree returned by the parser. */
static void json_free(JsonValue *v);
/* ------------------------------------------------------------------ */
/* Allocation helpers. */
/* ------------------------------------------------------------------ */
static void *xrealloc(void *ptr, size_t size) {
void *p = realloc(ptr, size);
if (p == NULL) {
fprintf(stderr, "json-formatter: out of memory\n");
exit(EXIT_FAILURE);
}
return p;
}
static char *xstrdup(const char *s) {
size_t n = strlen(s) + 1;
char *copy = xrealloc(NULL, n);
memcpy(copy, s, n);
return copy;
}
/* Growable, always NUL-terminated byte buffer. */
typedef struct {
char *data;
size_t len, cap;
} Buf;
static void buf_reserve(Buf *b, size_t extra) {
if (b->len + extra + 1 > b->cap) {
size_t cap = b->cap ? b->cap : 64;
while (b->len + extra + 1 > cap) cap *= 2;
b->data = xrealloc(b->data, cap);
b->cap = cap;
}
}
static void buf_push(Buf *b, char c) {
buf_reserve(b, 1);
b->data[b->len++] = c;
b->data[b->len] = '\0';
}
static void buf_puts(Buf *b, const char *s) {
size_t n = strlen(s);
buf_reserve(b, n);
memcpy(b->data + b->len, s, n);
b->len += n;
b->data[b->len] = '\0';
}
static JsonValue *json_new(JsonType type) {
JsonValue *v = xrealloc(NULL, sizeof *v);
memset(v, 0, sizeof *v);
v->type = type;
return v;
}
static void json_free(JsonValue *v) {
if (v == NULL) return;
switch (v->type) {
case JSON_STRING:
free(v->as.string);
break;
case JSON_ARRAY:
for (size_t i = 0; i < v->as.array.len; i++) json_free(v->as.array.items[i]);
free(v->as.array.items);
break;
case JSON_OBJECT:
for (size_t i = 0; i < v->as.object.len; i++) {
free(v->as.object.items[i].key);
json_free(v->as.object.items[i].value);
}
free(v->as.object.items);
break;
default:
break;
}
free(v);
}
static void array_push(JsonArray *a, JsonValue *v) {
if (a->len == a->cap) {
a->cap = a->cap ? a->cap * 2 : 8;
a->items = xrealloc(a->items, a->cap * sizeof *a->items);
}
a->items[a->len++] = v;
}
static void object_push(JsonObject *o, char *key, JsonValue *v) {
if (o->len == o->cap) {
o->cap = o->cap ? o->cap * 2 : 8;
o->items = xrealloc(o->items, o->cap * sizeof *o->items);
}
o->items[o->len].key = key;
o->items[o->len].value = v;
o->len++;
}
/* ------------------------------------------------------------------ */
/* Serializer. */
/* ------------------------------------------------------------------ */
static void write_indent(Buf *out, int depth, const char *pad) {
for (int i = 0; i < depth; i++) buf_puts(out, pad);
}
/* Emit a JSON string literal, escaping per RFC 8259. Like JSON.stringify it
* leaves '/' unescaped and emits control characters below 0x20 as \uXXXX. */
static void write_json_string(Buf *out, const char *s) {
buf_push(out, '"');
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
unsigned char c = *p;
switch (c) {
case '"': buf_puts(out, "\\\""); break;
case '\\': buf_puts(out, "\\\\"); break;
case '\n': buf_puts(out, "\\n"); break;
case '\r': buf_puts(out, "\\r"); break;
case '\t': buf_puts(out, "\\t"); break;
case '\b': buf_puts(out, "\\b"); break;
case '\f': buf_puts(out, "\\f"); break;
default:
if (c < 0x20) {
char esc[8];
snprintf(esc, sizeof esc, "\\u%04x", c);
buf_puts(out, esc);
} else {
buf_push(out, (char)c);
}
}
}
buf_push(out, '"');
}
/* Print a double the way JavaScript most often renders it: the shortest
* %g form that round-trips (try %.15g first, fall back to %.17g). */
static void write_number(Buf *out, double n) {
char tmp[40];
snprintf(tmp, sizeof tmp, "%.15g", n);
if (strtod(tmp, NULL) != n)
snprintf(tmp, sizeof tmp, "%.17g", n);
buf_puts(out, tmp);
}
static void write_compact(const JsonValue *v, Buf *out) {
switch (v->type) {
case JSON_NULL: buf_puts(out, "null"); break;
case JSON_BOOL: buf_puts(out, v->as.boolean ? "true" : "false"); break;
case JSON_NUMBER: write_number(out, v->as.number); break;
case JSON_STRING: write_json_string(out, v->as.string); break;
case JSON_ARRAY:
buf_push(out, '[');
for (size_t i = 0; i < v->as.array.len; i++) {
if (i) buf_push(out, ',');
write_compact(v->as.array.items[i], out);
}
buf_push(out, ']');
break;
case JSON_OBJECT:
buf_push(out, '{');
for (size_t i = 0; i < v->as.object.len; i++) {
if (i) buf_push(out, ',');
write_json_string(out, v->as.object.items[i].key);
buf_push(out, ':');
write_compact(v->as.object.items[i].value, out);
}
buf_push(out, '}');
break;
}
}
static void write_pretty(const JsonValue *v, Buf *out, int depth, const char *pad) {
switch (v->type) {
case JSON_ARRAY:
if (v->as.array.len == 0) { buf_puts(out, "[]"); return; }
buf_push(out, '[');
for (size_t i = 0; i < v->as.array.len; i++) {
if (i) buf_push(out, ',');
buf_push(out, '\n');
write_indent(out, depth + 1, pad);
write_pretty(v->as.array.items[i], out, depth + 1, pad);
}
buf_push(out, '\n');
write_indent(out, depth, pad);
buf_push(out, ']');
break;
case JSON_OBJECT:
if (v->as.object.len == 0) { buf_puts(out, "{}"); return; }
buf_push(out, '{');
for (size_t i = 0; i < v->as.object.len; i++) {
if (i) buf_push(out, ',');
buf_push(out, '\n');
write_indent(out, depth + 1, pad);
write_json_string(out, v->as.object.items[i].key);
buf_puts(out, ": ");
write_pretty(v->as.object.items[i].value, out, depth + 1, pad);
}
buf_push(out, '\n');
write_indent(out, depth, pad);
buf_push(out, '}');
break;
default:
/* Scalars and empty containers render identically in both modes. */
write_compact(v, out);
}
}
/* ------------------------------------------------------------------ */
/* Parser — a minimal recursive-descent JSON parser. */
/* ------------------------------------------------------------------ */
typedef struct {
size_t offset;
const char *message;
} ParseError;
typedef struct {
const char *bytes; /* NUL-terminated input */
size_t pos;
ParseError err; /* set when a parse function failed */
} Parser;
/* Record a failure at the current position; returns false for the
* boolean-returning helpers. */
static bool fail(Parser *p, const char *message) {
p->err.offset = p->pos;
p->err.message = message;
return false;
}
static char peek(const Parser *p) { return p->bytes[p->pos]; }
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
static void skip_ws(Parser *p) {
while (peek(p) == ' ' || peek(p) == '\t' || peek(p) == '\n' || peek(p) == '\r')
p->pos++;
}
/* Match a literal keyword at the current position; on success advance. */
static bool match_kw(Parser *p, const char *kw) {
size_t n = strlen(kw);
if (strncmp(p->bytes + p->pos, kw, n) == 0) {
p->pos += n;
return true;
}
return false;
}
/* Encode a code point as UTF-8 into the buffer. */
static void utf8_encode(Buf *b, uint32_t cp) {
if (cp < 0x80) {
buf_push(b, (char)cp);
} else if (cp < 0x800) {
buf_push(b, (char)(0xC0 | (cp >> 6)));
buf_push(b, (char)(0x80 | (cp & 0x3F)));
} else if (cp < 0x10000) {
buf_push(b, (char)(0xE0 | (cp >> 12)));
buf_push(b, (char)(0x80 | ((cp >> 6) & 0x3F)));
buf_push(b, (char)(0x80 | (cp & 0x3F)));
} else {
buf_push(b, (char)(0xF0 | (cp >> 18)));
buf_push(b, (char)(0x80 | ((cp >> 12) & 0x3F)));
buf_push(b, (char)(0x80 | ((cp >> 6) & 0x3F)));
buf_push(b, (char)(0x80 | (cp & 0x3F)));
}
}
/* Read exactly four hexadecimal digits following a '\u'. */
static bool parse_hex4(Parser *p, uint32_t *out) {
uint32_t value = 0;
for (int i = 0; i < 4; i++) {
char c = peek(p);
uint32_t d;
if (c >= '0' && c <= '9') d = (uint32_t)(c - '0');
else if (c >= 'a' && c <= 'f') d = (uint32_t)(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') d = (uint32_t)(c - 'A' + 10);
else return fail(p, "invalid hex digit in '\\u' escape");
value = value * 16 + d;
p->pos++;
}
*out = value;
return true;
}
/* Enforce correct UTF-16 surrogate pairing for '\u' escapes. */
static bool parse_unicode_escape(Parser *p, Buf *b) {
uint32_t cp;
if (!parse_hex4(p, &cp)) return false;
if (cp >= 0xD800 && cp <= 0xDBFF) {
if (peek(p) != '\\') return fail(p, "dangling high surrogate");
p->pos++;
if (peek(p) != 'u') return fail(p, "expected '\\u' for surrogate pair");
p->pos++;
uint32_t lo;
if (!parse_hex4(p, &lo)) return false;
if (lo < 0xDC00 || lo > 0xDFFF)
return fail(p, "invalid low surrogate after high surrogate");
utf8_encode(b, 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00));
return true;
}
if (cp >= 0xDC00 && cp <= 0xDFFF)
return fail(p, "unexpected low surrogate");
utf8_encode(b, cp);
return true;
}
static bool parse_escape(Parser *p, Buf *b) {
switch (peek(p)) {
case '"': p->pos++; buf_push(b, '"'); return true;
case '\\': p->pos++; buf_push(b, '\\'); return true;
case '/': p->pos++; buf_push(b, '/'); return true;
case 'b': p->pos++; buf_push(b, '\b'); return true;
case 'f': p->pos++; buf_push(b, '\f'); return true;
case 'n': p->pos++; buf_push(b, '\n'); return true;
case 'r': p->pos++; buf_push(b, '\r'); return true;
case 't': p->pos++; buf_push(b, '\t'); return true;
case 'u': p->pos++; return parse_unicode_escape(p, b);
default: return fail(p, "invalid escape sequence");
}
}
/* Parse a string literal (opening quote already peeked); returns a freshly
* allocated NUL-terminated string, or NULL on failure. */
static char *parse_string_raw(Parser *p) {
p->pos++; /* opening '"' */
Buf b = {0};
for (;;) {
char c = peek(p);
if (c == '\0') { fail(p, "unterminated string"); goto err; }
if (c == '"') { p->pos++; return b.data ? b.data : xstrdup(""); }
if (c == '\\') {
p->pos++;
if (!parse_escape(p, &b)) goto err;
} else if ((unsigned char)c < 0x20) {
fail(p, "unescaped control character in string");
goto err;
} else {
buf_push(&b, c);
p->pos++;
}
}
err:
free(b.data);
return NULL;
}
static JsonValue *parse_value(Parser *p);
static JsonValue *parse_object(Parser *p) {
p->pos++; /* consume '{' */
skip_ws(p);
JsonValue *v = json_new(JSON_OBJECT);
if (peek(p) == '}') { p->pos++; return v; }
for (;;) {
skip_ws(p);
if (peek(p) != '"') { fail(p, "expected string key"); goto err; }
char *key = parse_string_raw(p);
if (key == NULL) goto err;
skip_ws(p);
if (peek(p) != ':') { free(key); fail(p, "expected ':' after key"); goto err; }
p->pos++;
skip_ws(p);
JsonValue *value = parse_value(p);
if (value == NULL) { free(key); goto err; }
object_push(&v->as.object, key, value);
skip_ws(p);
if (peek(p) == ',') { p->pos++; continue; }
if (peek(p) == '}') { p->pos++; return v; }
fail(p, "expected ',' or '}'");
goto err;
}
err:
json_free(v);
return NULL;
}
static JsonValue *parse_array(Parser *p) {
p->pos++; /* consume '[' */
skip_ws(p);
JsonValue *v = json_new(JSON_ARRAY);
if (peek(p) == ']') { p->pos++; return v; }
for (;;) {
skip_ws(p);
JsonValue *item = parse_value(p);
if (item == NULL) goto err;
array_push(&v->as.array, item);
skip_ws(p);
if (peek(p) == ',') { p->pos++; continue; }
if (peek(p) == ']') { p->pos++; return v; }
fail(p, "expected ',' or ']'");
goto err;
}
err:
json_free(v);
return NULL;
}
static JsonValue *parse_number(Parser *p) {
size_t start = p->pos;
if (peek(p) == '-') p->pos++;
if (peek(p) == '0') {
p->pos++;
} else if (peek(p) >= '1' && peek(p) <= '9') {
while (is_digit(peek(p))) p->pos++;
} else {
p->err.offset = start;
p->err.message = "invalid number";
return NULL;
}
if (peek(p) == '.') {
p->pos++;
if (!is_digit(peek(p))) { fail(p, "expected digit after decimal point"); return NULL; }
while (is_digit(peek(p))) p->pos++;
}
if (peek(p) == 'e' || peek(p) == 'E') {
p->pos++;
if (peek(p) == '+' || peek(p) == '-') p->pos++;
if (!is_digit(peek(p))) { fail(p, "expected digit in exponent"); return NULL; }
while (is_digit(peek(p))) p->pos++;
}
char tmp[64];
size_t n = p->pos - start;
if (n >= sizeof tmp) { fail(p, "number too long"); return NULL; }
memcpy(tmp, p->bytes + start, n);
tmp[n] = '\0';
char *end;
double value = strtod(tmp, &end);
if (*end != '\0') { fail(p, "invalid number"); return NULL; }
JsonValue *v = json_new(JSON_NUMBER);
v->as.number = value;
return v;
}
static JsonValue *parse_value(Parser *p) {
char c = peek(p);
if (c == '-' || is_digit(c)) return parse_number(p);
switch (c) {
case '{': return parse_object(p);
case '[': return parse_array(p);
case '"': {
char *s = parse_string_raw(p);
if (s == NULL) return NULL;
JsonValue *v = json_new(JSON_STRING);
v->as.string = s;
return v;
}
case 't': case 'f': {
bool is_true = match_kw(p, "true");
if (!is_true && !match_kw(p, "false")) { fail(p, "invalid literal"); return NULL; }
JsonValue *v = json_new(JSON_BOOL);
v->as.boolean = is_true;
return v;
}
case 'n':
if (!match_kw(p, "null")) { fail(p, "invalid literal"); return NULL; }
return json_new(JSON_NULL);
default:
fail(p, "unexpected token");
return NULL;
}
}
static JsonValue *parse_document(Parser *p) {
skip_ws(p);
JsonValue *v = parse_value(p);
if (v == NULL) return NULL;
skip_ws(p);
if (peek(p) != '\0') {
json_free(v);
fail(p, "unexpected trailing characters");
return NULL;
}
return v;
}
/* ------------------------------------------------------------------ */
/* Entry point. */
/* ------------------------------------------------------------------ */
static FormatResult format_json(const char *input, Mode mode, int indent) {
/* Trim leading/trailing JSON whitespace. */
const char *ws = " \t\r\n";
const char *start = input;
const char *end = input + strlen(input);
while (start < end && strchr(ws, *start)) start++;
while (end > start && strchr(ws, end[-1])) end--;
size_t len = (size_t)(end - start);
if (len == 0)
return (FormatResult){ .output = xstrdup(""), .error = NULL };
/* The parser works on a NUL-terminated copy of the trimmed input. */
char *raw = xrealloc(NULL, len + 1);
memcpy(raw, start, len);
raw[len] = '\0';
Parser p = { .bytes = raw, .pos = 0 };
JsonValue *value = parse_document(&p);
free(raw);
if (value == NULL) {
char msg[128];
snprintf(msg, sizeof msg, "%s at byte %zu", p.err.message, p.err.offset);
return (FormatResult){ .output = xstrdup(""), .error = xstrdup(msg) };
}
Buf pad = {0};
for (int i = 0; i < indent; i++) buf_push(&pad, ' ');
Buf out = {0};
if (mode == MODE_MINIFY)
write_compact(value, &out);
else
write_pretty(value, &out, 0, pad.data ? pad.data : "");
free(pad.data);
json_free(value);
return (FormatResult){ .output = out.data ? out.data : xstrdup(""), .error = NULL };
}
int main(void) {
const char *sample =
"{\"name\":\"CosmoDev\",\"tools\":[\"json\",\"base64\"],"
"\"free\":true,\"nested\":{\"ok\":1}}";
FormatResult result = format_json(sample, MODE_BEAUTIFY, 2);
if (result.error != NULL)
printf("error: %s\n", result.error);
else
printf("%s\n", result.output);
free(result.output);
free(result.error);
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 →