"Is this valid JSON?" actually asks two different questions, and mixing them up is where most validation bugs come from. One is syntax: does the string parse as JSON at all? The other is shape: does the parsed value have the fields, types, and structure your code actually needs? JSON.parse only answers the first one.
Syntax validation: JSON.parse in a try/catch
JSON.parse throws a SyntaxError on malformed input, so wrapping it in a try/catch is the entire mechanism for checking whether a string is parseable JSON:
function isValidJson(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
isValidJson('{"a":1}'); // true
isValidJson('{"a":1,}'); // false — trailing comma
isValidJson("{bad json"); // falseThe error message tells you where parsing gave up — V8 (Chrome, Node, Edge) reports it as Expected property name or '}' in JSON at position 1 for the trailing-comma case above. For the full list of syntax mistakes that trigger this and how to read the message, see Common JSON Errors and How to Fix Them.
A common misconception: JSON isn't just objects and arrays
It's easy to assume valid JSON always starts with { or [, but the JSON spec allows any value at the top level — a bare number, a quoted string, true, false, or null are all complete, valid JSON documents on their own:
isValidJson("42"); // true
isValidJson('"hello"'); // true — note the string is itself quoted
isValidJson("null"); // true
isValidJson("hello"); // false — unquoted, not valid JSON
isValidJson("undefined"); // false — not JSON; that's a JS-only value
isValidJson("NaN"); // false — same reasonThat last pair matters if you're validating data that might have come from JSON.stringify'd JavaScript rather than a spec-compliant source: undefined and NaN have no JSON representation. JSON.stringify silently drops undefined object properties and turns NaN/Infinity into null — so round-tripping through JSON can lose information even when nothing throws.
Syntax-valid isn't shape-valid
This passes isValidJson without any error:
isValidJson('{"username": 42, "email": null}'); // trueIt's perfectly well-formed JSON — and almost certainly not what a function expecting { username: string, email: string } wants. A try/catch around JSON.parse catches typos and malformed strings; it says nothing about whether the right fields are present, or the right type.
Hand-rolled shape validation
For a handful of required fields, a manual check after parsing is often enough:
function parseUser(str) {
const data = JSON.parse(str); // throws on bad syntax
if (typeof data !== "object" || data === null || Array.isArray(data)) {
throw new Error("expected a JSON object");
}
if (typeof data.username !== "string" || data.username.length === 0) {
throw new Error("username must be a non-empty string");
}
if (typeof data.email !== "string") {
throw new Error("email must be a string");
}
return data;
}Note the typeof data !== "object" check alone isn't enough — typeof null === "object" in JavaScript, and Array.isArray also passes a plain typeof object check, so both need an explicit guard if the shape you want is specifically a non-null, non-array object.
This approach scales poorly past a few fields: nested objects, optional properties, arrays of a specific shape, and cross-field rules turn into a wall of if statements that's tedious to write and easy to get subtly wrong.
JSON Schema for anything bigger
Past that point, describing the shape declaratively as a JSON Schema and validating against it with a library — ajv is the standard choice in JavaScript — reads better and catches more:
import Ajv from "ajv";
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
username: { type: "string", minLength: 1 },
age: { type: "number", minimum: 0 },
},
required: ["username", "age"],
additionalProperties: false,
};
const validate = ajv.compile(schema);
const valid = validate({ username: "ada", age: -1 });
console.log(valid); // false
console.log(validate.errors);
// [{ instancePath: "/age", keyword: "minimum", message: "must be >= 0", ... }]validate.errors lists every failing rule with its exact path in the document — not just the first one, which matters when you want to show a user everything wrong with their input at once rather than one error per resubmission. For how that keyword-checking actually works under the hood — why it doesn't stop at the first failure, and how $ref/$defs let you reuse schemas — see JSON Schema Validation Explained. Don't have a schema yet? JSON Schema Generator writes a first draft from a sample document.
Common mistakes specific to this case
- Treating a successful
JSON.parseas full validation. It proves the string is syntactically valid JSON, nothing about whether the resulting value has the shape the rest of the code assumes. - Checking
typeof value === "object"without excludingnulland arrays. Both pass that check; add an explicitvalue !== nulland!Array.isArray(value)guard if the shape you want excludes them. - Parsing a value that's already an object. If the JSON already went through
JSON.parseonce — a framework that auto-parses request bodies, for instance — callingJSON.parseagain throws, since it now receives an object, not a string. - Skipping validation on trusted-looking input. A request body, a
localStoragevalue, a config file someone hand-edited — all are just strings from JSON's perspective, and none are guaranteed to match the shape your code expects just because they usually do.
Try it yourself
For a syntax check with the exact line and column of the first error, use JSON Validator. To check that a document also has the right shape, paste it and a schema into JSON Schema Validator — it reports every failing rule and where it failed, the same information ajv gives you in code. Both run entirely in your browser; nothing you paste is uploaded.