JSON Viewer

JSON Schema — check your data before it breaks something

Write a schema that catches bad data, with the keywords that matter and the traps that waste a day.

Updated 2026-11-04 · 8 minute read

A type in your program checks your own code. A JSON Schema checks the data that arrives from outside it. That is a different job, and it is the one that saves you at three in the morning.

The smallest useful schema

{
  "type": "object",
  "required": ["id", "email"],
  "properties": {
    "id": { "type": "integer" },
    "email": { "type": "string" }
  }
}

This says: the value is an object, it must have id and email, id is a whole number and email is text. Any other key is allowed, because JSON Schema allows what it does not forbid.

That last sentence is the one people learn the hard way. A schema permits by default. If you write only {"type": "object"}, every object passes.

The keywords worth knowing

For every value

Keyword Meaning
type object, array, string, number, integer, boolean, null
enum One of this list of values
const Exactly this value

type also takes a list: "type": ["string", "null"] allows either.

For text

{
  "type": "string",
  "minLength": 1,
  "maxLength": 100,
  "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$"
}

pattern is a regular expression. Remember to escape the backslash, because the schema is itself JSON.

For numbers

{
  "type": "number",
  "minimum": 0,
  "exclusiveMaximum": 100,
  "multipleOf": 0.5
}

For lists

{
  "type": "array",
  "minItems": 1,
  "maxItems": 50,
  "uniqueItems": true,
  "items": { "type": "string" }
}

items describes every item. prefixItems describes the first few by position, which is how you type a pair such as [latitude, longitude].

For objects

{
  "type": "object",
  "required": ["id"],
  "properties": { "id": { "type": "integer" } },
  "patternProperties": { "^x-": { "type": "string" } },
  "additionalProperties": false,
  "minProperties": 1
}

"additionalProperties": false is the line that catches typing mistakes. Without it, {"emial": "..."} passes a schema that requires email as long as email is not in required. With it, the unknown key is reported.

Be careful: it also refuses a new field that a newer version of the API adds. Use it on data you own. Think twice on data from somebody else.

Combining

{ "anyOf": [ {"type": "string"}, {"type": "number"} ] }

oneOf is the one people get wrong. If two of your options can both match, oneOf fails even though the data looks right. Use anyOf unless you really need "exactly one".

Conditions

{
  "if": { "properties": { "kind": { "const": "card" } }, "required": ["kind"] },
  "then": { "required": ["cardNumber"] },
  "else": { "required": ["accountNumber"] }
}

This is how you say "if it is a card payment, a card number is required".

Reuse

{
  "$defs": {
    "money": {
      "type": "object",
      "required": ["amount", "currency"],
      "properties": {
        "amount": { "type": "number" },
        "currency": { "type": "string", "minLength": 3, "maxLength": 3 }
      }
    }
  },
  "properties": {
    "price": { "$ref": "#/$defs/money" },
    "tax": { "$ref": "#/$defs/money" }
  }
}

Write a shape once and point at it. $ref with a # is a pointer inside the same document.

Testing a schema here

Open your data in the viewer, then Tools → Check against a JSON Schema. Paste the schema and press Check.

Every place that does not fit appears in a list with its path and a plain message, and each place is marked in the tree. Click a line and the tree moves to it.

This is the fast way to write a schema: start with {"type": "object"}, add a rule, press Check, and watch what it catches. A schema written against real data is right the first time, and a schema written from imagination usually is not.

The check runs in your browser, so neither the data nor the schema is uploaded. It does not fetch a $ref that points at another file, for the same reason, and it says so.

The traps

A schema permits what it does not forbid. Say this to yourself twice.

required is separate from properties. Describing a key does not make it required, and requiring a key does not describe it. You usually need both.

"type": "integer" means a whole number, so 1.0 passes and 1.5 does not.

format is usually only a hint. {"format": "email"} is ignored by many validators unless you turn the check on. Use pattern when it must be enforced.

A number in a schema is a JavaScript number. An identifier of 20 digits cannot be checked exactly by minimum or maximum. Treat such an identifier as text, in your schema and in your data.

Say which draft you use. Put "$schema": "https://json-schema.org/draft/2020-12/schema" at the top. Draft 7 and 2020-12 differ in how lists are described, and a validator that guesses wrong gives a confusing message.

Where to put the check

  1. At the edge of your service. Check the body of every request before anything else touches it.
  2. In your tests. Keep a real answer and check it against the schema, so a change in the API breaks a test and not production.
  3. In your build. Check your own configuration files. A typing mistake in a configuration file is caught in one second instead of one deploy.

Where to go next

Try it on your own file. The tool runs in your browser. Nothing is uploaded, and nothing is stored on a server.

Open the JSON Viewer

Read next

Why big numbers break in JSON, and what to do

An identifier of 19 digits goes into a parser and comes out changed. Here is why, and the three ways to stop it.

Is it safe to paste JSON into an online viewer?

What really happens to your data in a web tool, how to tell an upload from local work, and when to say no.