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++ (C++17, standard library only — the STL ships no JSON)
// 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
// std::vector<std::pair<...>>, 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 <cstdint>
#include <cstdio>
#include <cstdlib>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

// ---------------------------------------------------------------------
// Public surface — mirrors the TypeScript { output, error } shape.
// ---------------------------------------------------------------------

enum class Mode { Beautify, Minify };

struct FormatResult {
    std::string output;                  // "" on empty input or parse error
    std::optional<std::string> error;    // engaged = parse failure
};

// A parsed JSON value. Object members live in a vector of pairs to preserve
// source insertion order just like a JavaScript engine.
struct JsonValue;
using JsonArray = std::vector<JsonValue>;
using JsonObject = std::vector<std::pair<std::string, JsonValue>>;

struct JsonValue {
    std::variant<std::nullptr_t, bool, double, std::string, JsonArray, JsonObject> v;
};

// ---------------------------------------------------------------------
// Parser.
// ---------------------------------------------------------------------

// A parse failure: byte offset + message.
struct ParseError : std::runtime_error {
    size_t offset;
    ParseError(size_t off, const char* msg) : std::runtime_error(msg), offset(off) {}
};

class Parser {
public:
    explicit Parser(std::string_view text) : text_(text) {}

    JsonValue parseDocument() {
        skipWs();
        JsonValue value = parseValue();
        skipWs();
        if (pos_ < text_.size()) fail("unexpected trailing characters");
        return value;
    }

private:
    [[noreturn]] void fail(const char* message) { throw ParseError(pos_, message); }
    [[noreturn]] void failAt(size_t offset, const char* message) { throw ParseError(offset, message); }

    char peek() const { return pos_ < text_.size() ? text_[pos_] : '\0'; }
    void bump() { pos_++; }
    static bool isDigit(char c) { return c >= '0' && c <= '9'; }

    void skipWs() {
        while (peek() == ' ' || peek() == '\t' || peek() == '\n' || peek() == '\r') bump();
    }

    // Match a literal keyword at the current position; on success advance.
    bool matchKw(std::string_view kw) {
        if (text_.substr(pos_, kw.size()) == kw) {
            pos_ += kw.size();
            return true;
        }
        return false;
    }

    JsonValue parseValue() {
        const char c = peek();
        if (c == '-' || isDigit(c)) return parseNumber();
        switch (c) {
            case '{': return parseObject();
            case '[': return parseArray();
            case '"': return JsonValue{parseString()};
            case 't': case 'f': return parseBool();
            case 'n': return parseNull();
            default: fail("unexpected token");
        }
    }

    JsonValue parseObject() {
        bump(); // consume '{'
        skipWs();
        JsonObject members;
        if (peek() == '}') {
            bump();
            return JsonValue{std::move(members)};
        }
        for (;;) {
            skipWs();
            if (peek() != '"') fail("expected string key");
            std::string key = parseString();
            skipWs();
            if (peek() != ':') fail("expected ':' after key");
            bump();
            skipWs();
            members.emplace_back(std::move(key), parseValue());
            skipWs();
            if (peek() == ',') { bump(); continue; }
            if (peek() == '}') { bump(); break; }
            fail("expected ',' or '}'");
        }
        return JsonValue{std::move(members)};
    }

    JsonValue parseArray() {
        bump(); // consume '['
        skipWs();
        JsonArray items;
        if (peek() == ']') {
            bump();
            return JsonValue{std::move(items)};
        }
        for (;;) {
            skipWs();
            items.push_back(parseValue());
            skipWs();
            if (peek() == ',') { bump(); continue; }
            if (peek() == ']') { bump(); break; }
            fail("expected ',' or ']'");
        }
        return JsonValue{std::move(items)};
    }

    std::string parseString() {
        bump(); // opening '"'
        std::string s;
        for (;;) {
            const char c = peek();
            if (c == '\0') fail("unterminated string");
            if (c == '"') { bump(); return s; }
            if (c == '\\') {
                bump();
                parseEscape(s);
            } else if (static_cast<unsigned char>(c) < 0x20) {
                fail("unescaped control character in string");
            } else {
                s.push_back(c);
                bump();
            }
        }
    }

    void parseEscape(std::string& s) {
        switch (peek()) {
            case '"':  bump(); s.push_back('"');  return;
            case '\\': bump(); s.push_back('\\'); return;
            case '/':  bump(); s.push_back('/');  return;
            case 'b':  bump(); s.push_back('\b'); return;
            case 'f':  bump(); s.push_back('\f'); return;
            case 'n':  bump(); s.push_back('\n'); return;
            case 'r':  bump(); s.push_back('\r'); return;
            case 't':  bump(); s.push_back('\t'); return;
            case 'u':  bump(); parseUnicodeEscape(s); return;
            default: fail("invalid escape sequence");
        }
    }

    // Enforce correct UTF-16 surrogate pairing for '\u' escapes.
    void parseUnicodeEscape(std::string& s) {
        const auto cp = parseHex4();
        if (cp >= 0xD800 && cp <= 0xDBFF) {
            if (peek() != '\\') fail("dangling high surrogate");
            bump();
            if (peek() != 'u') fail("expected '\\u' for surrogate pair");
            bump();
            const auto lo = parseHex4();
            if (lo < 0xDC00 || lo > 0xDFFF)
                fail("invalid low surrogate after high surrogate");
            appendUtf8(s, 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00));
        } else if (cp >= 0xDC00 && cp <= 0xDFFF) {
            fail("unexpected low surrogate");
        } else {
            appendUtf8(s, cp);
        }
    }

    // Read exactly four hexadecimal digits following a '\u'.
    uint32_t parseHex4() {
        uint32_t value = 0;
        for (int i = 0; i < 4; i++) {
            const char c = peek();
            uint32_t d;
            if (c >= '0' && c <= '9') d = static_cast<uint32_t>(c - '0');
            else if (c >= 'a' && c <= 'f') d = static_cast<uint32_t>(c - 'a' + 10);
            else if (c >= 'A' && c <= 'F') d = static_cast<uint32_t>(c - 'A' + 10);
            else fail("invalid hex digit in '\\u' escape");
            value = value * 16 + d;
            bump();
        }
        return value;
    }

    static void appendUtf8(std::string& s, uint32_t cp) {
        if (cp < 0x80) {
            s.push_back(static_cast<char>(cp));
        } else if (cp < 0x800) {
            s.push_back(static_cast<char>(0xC0 | (cp >> 6)));
            s.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
        } else if (cp < 0x10000) {
            s.push_back(static_cast<char>(0xE0 | (cp >> 12)));
            s.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
            s.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
        } else {
            s.push_back(static_cast<char>(0xF0 | (cp >> 18)));
            s.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
            s.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
            s.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
        }
    }

    JsonValue parseNumber() {
        const size_t start = pos_;
        if (peek() == '-') bump();
        if (peek() == '0') {
            bump();
        } else if (peek() >= '1' && peek() <= '9') {
            while (isDigit(peek())) bump();
        } else {
            failAt(start, "invalid number");
        }
        if (peek() == '.') {
            bump();
            if (!isDigit(peek())) fail("expected digit after decimal point");
            while (isDigit(peek())) bump();
        }
        if (peek() == 'e' || peek() == 'E') {
            bump();
            if (peek() == '+' || peek() == '-') bump();
            if (!isDigit(peek())) fail("expected digit in exponent");
            while (isDigit(peek())) bump();
        }
        const std::string text{text_.substr(start, pos_ - start)};
        try {
            return JsonValue{std::stod(text)};
        } catch (const std::exception&) {
            fail("invalid number");
        }
    }

    JsonValue parseBool() {
        if (matchKw("true")) return JsonValue{true};
        if (matchKw("false")) return JsonValue{false};
        fail("invalid literal");
    }

    JsonValue parseNull() {
        if (matchKw("null")) return JsonValue{nullptr};
        fail("invalid literal");
    }

    std::string_view text_;
    size_t pos_ = 0;
};

// ---------------------------------------------------------------------
// Serializer.
// ---------------------------------------------------------------------

// The classic C++17 "overloaded" visitor helper for std::visit.
template <class... Ts>
struct Overloaded : Ts... {
    using Ts::operator()...;
};
template <class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;

void pushIndent(std::string& out, size_t depth, std::string_view pad) {
    for (size_t i = 0; i < depth; i++) out.append(pad);
}

// Emit a JSON string literal, escaping per RFC 8259. Like JSON.stringify it
// leaves '/' unescaped and emits control characters below 0x20 as \uXXXX.
void writeJsonString(std::string& out, std::string_view s) {
    out.push_back('"');
    for (const char raw : s) {
        const auto c = static_cast<unsigned char>(raw);
        switch (c) {
            case '"':  out += "\\\""; break;
            case '\\': out += "\\\\"; break;
            case '\n': out += "\\n"; break;
            case '\r': out += "\\r"; break;
            case '\t': out += "\\t"; break;
            case '\b': out += "\\b"; break;
            case '\f': out += "\\f"; break;
            default:
                if (c < 0x20) {
                    char esc[8];
                    std::snprintf(esc, sizeof esc, "\\u%04x", c);
                    out += esc;
                } else {
                    out.push_back(raw);
                }
        }
    }
    out.push_back('"');
}

// Print a double the way JavaScript most often renders it: the shortest
// %g form that round-trips (try %.15g first, fall back to %.17g).
void writeNumber(std::string& out, double n) {
    char tmp[40];
    std::snprintf(tmp, sizeof tmp, "%.15g", n);
    if (std::strtod(tmp, nullptr) != n)
        std::snprintf(tmp, sizeof tmp, "%.17g", n);
    out += tmp;
}

void writeCompact(const JsonValue& value, std::string& out) {
    std::visit(Overloaded{
                   [&](std::nullptr_t) { out += "null"; },
                   [&](bool b) { out += b ? "true" : "false"; },
                   [&](double n) { writeNumber(out, n); },
                   [&](const std::string& s) { writeJsonString(out, s); },
                   [&](const JsonArray& items) {
                       out.push_back('[');
                       for (size_t i = 0; i < items.size(); i++) {
                           if (i > 0) out.push_back(',');
                           writeCompact(items[i], out);
                       }
                       out.push_back(']');
                   },
                   [&](const JsonObject& members) {
                       out.push_back('{');
                       for (size_t i = 0; i < members.size(); i++) {
                           if (i > 0) out.push_back(',');
                           writeJsonString(out, members[i].first);
                           out.push_back(':');
                           writeCompact(members[i].second, out);
                       }
                       out.push_back('}');
                   },
               },
               value.v);
}

void writePretty(const JsonValue& value, std::string& out, size_t depth, std::string_view pad) {
    if (const auto* items = std::get_if<JsonArray>(&value.v); items && !items->empty()) {
        out.push_back('[');
        for (size_t i = 0; i < items->size(); i++) {
            if (i > 0) out.push_back(',');
            out.push_back('\n');
            pushIndent(out, depth + 1, pad);
            writePretty((*items)[i], out, depth + 1, pad);
        }
        out.push_back('\n');
        pushIndent(out, depth, pad);
        out.push_back(']');
    } else if (const auto* members = std::get_if<JsonObject>(&value.v); members && !members->empty()) {
        out.push_back('{');
        for (size_t i = 0; i < members->size(); i++) {
            if (i > 0) out.push_back(',');
            out.push_back('\n');
            pushIndent(out, depth + 1, pad);
            writeJsonString(out, (*members)[i].first);
            out += ": ";
            writePretty((*members)[i].second, out, depth + 1, pad);
        }
        out.push_back('\n');
        pushIndent(out, depth, pad);
        out.push_back('}');
    } else {
        // Scalars and empty containers render identically in both modes.
        writeCompact(value, out);
    }
}

// ---------------------------------------------------------------------
// Entry point.
// ---------------------------------------------------------------------

// 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.
FormatResult formatJson(std::string_view input, Mode mode, int indent) {
    constexpr const char* ws = " \t\r\n";
    const size_t begin = input.find_first_not_of(ws);
    if (begin == std::string_view::npos)
        return FormatResult{.output = std::string{}};
    const size_t end = input.find_last_not_of(ws);
    const std::string_view raw = input.substr(begin, end - begin + 1);

    try {
        const JsonValue value = Parser(raw).parseDocument();
        std::string pad(static_cast<size_t>(indent), ' ');
        std::string out;
        switch (mode) {
            case Mode::Minify: writeCompact(value, out); break;
            case Mode::Beautify: writePretty(value, out, 0, pad); break;
        }
        return FormatResult{.output = std::move(out)};
    } catch (const ParseError& e) {
        char where[32];
        std::snprintf(where, sizeof where, " at byte %zu", e.offset);
        return FormatResult{.output = std::string{}, .error = e.what() + std::string(where)};
    }
}

int main() {
    const std::string sample =
        R"({"name":"CosmoDev","tools":["json","base64"],"free":true,"nested":{"ok":1}})";
    const FormatResult result = formatJson(sample, Mode::Beautify, 2);
    if (result.error)
        std::printf("error: %s\n", result.error->c_str());
    else
        std::printf("%s\n", result.output.c_str());
    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 →