Skip to content

JSON Validator — C++ source

Validate JSON and pinpoint errors with line and column numbers. Clear valid/invalid verdict plus the exact error location - runs entirely in your browser.

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

// json-validator — C++17 port: hand-rolled recursive-descent JSON validator, ported from src/lib/jsonValidate.ts.
#include <cstdio>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

struct Result {
    bool valid;
    std::string error;                // empty when valid
    std::optional<int> line, column;  // nullopt = no position reported
};

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

    Result validate() {
        ws();
        if (i_ >= text_.size()) return {false, "Input is empty", {}, {}};
        bool ok = value();
        if (ok) { ws(); ok = i_ >= text_.size(); } // reject trailing characters
        if (!ok) {
            auto [line, col] = lineCol(i_); // 1-based; "\r\n" counts as one break
            return {false, "Unexpected token", line, col};
        }
        return {true, "", {}, {}};
    }

private:
    static constexpr std::string_view WS = " \t\r\n";
    void ws() { while (i_ < text_.size() && WS.find(text_[i_]) != std::string_view::npos) ++i_; }
    char peek() const { return i_ < text_.size() ? text_[i_] : '\0'; }
    bool digit() const { return i_ < text_.size() && text_[i_] >= '0' && text_[i_] <= '9'; }

    bool value() {
        ws();
        switch (peek()) {
            case '{': return object(); case '[': return array(); case '"': return string();
            case 't': return literal("true"); case 'f': return literal("false"); case 'n': return literal("null");
            default: return number();
        }
    }

    // Strings: any escape pair passes; a raw control char (< 0x20) fails.
    bool string() {
        ++i_; // opening quote
        while (i_ < text_.size() && text_[i_] != '"') {
            if (text_[i_] == '\\') { if (++i_ >= text_.size()) return false; }
            else if (static_cast<unsigned char>(text_[i_]) < 0x20) return false;
            ++i_;
        }
        return i_++ < text_.size(); // consume the closing quote, or fail at EOF
    }

    bool object() {
        ++i_; ws();
        if (peek() == '}') { ++i_; return true; }
        while (true) {
            ws();
            if (peek() != '"' || !string()) return false;
            ws();
            if (peek() != ':') return false;
            ++i_;
            if (!value()) return false;
            ws();
            if (peek() == ',') { ++i_; continue; }
            if (peek() == '}') { ++i_; return true; }
            return false;
        }
    }

    bool array() {
        ++i_; ws();
        if (peek() == ']') { ++i_; return true; }
        while (true) {
            if (!value()) return false;
            ws();
            if (peek() == ',') { ++i_; continue; }
            if (peek() == ']') { ++i_; return true; }
            return false;
        }
    }

    bool literal(std::string_view word) {
        if (text_.substr(i_, word.size()) != word) return false;
        i_ += word.size();
        return true;
    }

    // RFC 8259 number: -?int(.frac)?([eE][+-]?exp)?
    bool number() {
        if (peek() == '-') ++i_;
        if (!digit()) return false;
        if (text_[i_] == '0') ++i_; else while (digit()) ++i_;
        if (peek() == '.') { ++i_; if (!digit()) return false; while (digit()) ++i_; }
        if (peek() == 'e' || peek() == 'E') {
            ++i_;
            if (peek() == '+' || peek() == '-') ++i_;
            if (!digit()) return false;
            while (digit()) ++i_;
        }
        return true;
    }

    // 1-based line/column for a 0-based offset — the shared polyglot algorithm.
    std::pair<int, int> lineCol(size_t offset) const {
        int line = 1, col = 1;
        for (size_t j = 0; j < offset && j < text_.size(); ++j) {
            if (text_[j] == '\n') { ++line; col = 1; }
            else if (text_[j] == '\r') { ++line; col = 1; if (j + 1 < text_.size() && text_[j + 1] == '\n') ++j; }
            else ++col;
        }
        return {line, col};
    }

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

int main() {
    for (std::string_view sample : {"{\"a\": [1, 2, {\"b\": null}]}", "{\n  \"a\": ,\n}"}) {
        Result r = Validator(sample).validate();
        if (r.valid) std::puts("valid");
        else std::printf("invalid: %s (line %d, column %d)\n", r.error.c_str(), *r.line, *r.column);
    }
}

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 →