Skip to content

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 →