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 →