Skip to content

JSON Validator — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// json-validator — Java (17+) port: hand-rolled recursive-descent validator (the JDK has no JSON parser), ported from src/lib/jsonValidate.ts.
import java.util.List;

public final class JsonValidator {
    record Result(boolean valid, String error, Integer line, Integer column) {}

    private final String text;
    private int i;

    public static Result validate(String s) { return new JsonValidator(s).run(); }

    private JsonValidator(String text) { this.text = text; }

    private Result run() {
        ws();
        if (i >= text.length()) return new Result(false, "Input is empty", null, null);
        boolean ok = value();
        if (ok) { ws(); ok = i >= text.length(); } // reject trailing characters
        if (!ok) {
            int[] lc = lineCol(i); // 1-based; counts "\r\n" as one break
            return new Result(false, "Unexpected token", lc[0], lc[1]);
        }
        return new Result(true, null, null, null);
    }

    private void ws() { while (i < text.length() && " \t\r\n".indexOf(text.charAt(i)) >= 0) i++; }
    private char peek() { return i < text.length() ? text.charAt(i) : '\0'; }
    private boolean digit() { return i < text.length() && text.charAt(i) >= '0' && text.charAt(i) <= '9'; }

    private boolean 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.
    private boolean string() {
        i++; // opening quote
        while (i < text.length() && text.charAt(i) != '"') {
            if (text.charAt(i) == '\\') { if (++i >= text.length()) return false; }
            else if (text.charAt(i) < ' ') return false;
            i++;
        }
        return i++ < text.length(); // consume the closing quote, or fail at EOF
    }

    private boolean 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;
        }
    }

    private boolean 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;
        }
    }

    private boolean literal(String word) {
        if (!text.startsWith(word, i)) return false;
        i += word.length();
        return true;
    }

    // RFC 8259 number: -?int(.frac)?([eE][+-]?exp)?
    private boolean number() {
        if (peek() == '-') i++;
        if (!digit()) return false;
        if (text.charAt(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.
    private int[] lineCol(int offset) {
        int line = 1, col = 1;
        for (int j = 0; j < offset && j < text.length(); j++) {
            char c = text.charAt(j);
            if (c == '\n') { line++; col = 1; }
            else if (c == '\r') { line++; col = 1; if (j + 1 < text.length() && text.charAt(j + 1) == '\n') j++; }
            else col++;
        }
        return new int[]{line, col};
    }

    public static void main(String[] args) {
        for (String s : List.of("{\"a\": [1, 2, {\"b\": null}]}", "{\n  \"a\": ,\n}")) {
            Result r = validate(s);
            System.out.println(r.valid ? "valid" : "invalid: " + r.error + " (line " + r.line + ", column " + 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 →