by SchemaSafe (LX AI)

Last updated: 2026-09-16

Common JSON Schema authoring mistakes

JSON Schema, currently Draft 2020-12, is the standard way to describe and validate JSON shape. Most broken schemas are not syntax errors; they are permissive mistakes that pass invalid data. The four that surface most in code review are a missing required array, an open additionalProperties, misused type unions, and a $ref that points nowhere.

Forgetting the required array

When you add a property under properties you are only describing its shape, not demanding its presence. If the required array omits that key, an instance without the field validates successfully. This is a silent pass because the validator treats the property as optional. I have seen teams ship APIs where a critical identifier was missing from required, leading to downstream null‑pointer errors that only appeared in production.

How to spot the mistake

How to fix it

  1. Place required as a sibling of properties, not inside it.
  2. List every property that the contract depends on.
  3. If a property can truly be omitted, add it to required only when a sensible default exists, or model the optionality with nullable in the type array.
  4. Run the schema through a linter that flags missing required entries; many editors have plugins for this.
  5. For example, a user schema that expects an id should contain:

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

    Without the required line, an object with only {"name":"Ada"} would pass, even though the API later treats id as mandatory.

    Leaving additionalProperties open

    The default value of additionalProperties is true. This lets any key not defined in properties slip through validation. A typo such as "user_name" instead of "username" then becomes a silently accepted field, polluting the data model and making later migrations harder. I have seen teams spend weeks cleaning up stray keys that entered the database because the schema never rejected them.

    Detecting an open schema

    • Search for the keyword additionalProperties. If it is absent, the schema is open.
    • If present, verify the value is either false or a schema that constrains extra keys.
    • During code review, ask: “What should happen if a client sends a field we do not recognize?” If the answer is “reject it”, the schema must close the door.

    Closing the door safely

    1. Set additionalProperties: false when you want strict validation of known fields only.
    2. If you need to allow future extensions, provide a schema for them, e.g. additionalProperties: { "type": ["string","number"] } to restrict extra keys to primitive values.
    3. Consider using patternProperties when you want to allow keys that match a certain naming convention while rejecting others.
    4. Document the intention in a comment so future maintainers know why the schema is open or closed.

    Here is a strict version of a product schema:

    {
      "type": "object",
      "properties": {
        "sku": { "type": "string" },
        "price": { "type": "number", "minimum": 0 }
      },
      "required": ["sku","price"],
      "additionalProperties": false
    }
    

    With this schema, an instance containing {"sku":"ABC","price":10,"extra":1} fails validation, catching the typo early.

    Misusing type unions

    The type keyword expects either a string or an array of strings. Writing "type": "string, number" is invalid JSON Schema; the parser treats the whole value as a single unknown type, which many validators silently ignore, allowing any value to pass. Another common slip is omitting the type entirely for an optional field, which makes the validator skip type checking for that key.

    Signs of a broken union

    • The value of type contains a comma outside of quotes.
    • The value is a string that is not one of the recognized primitive types (string, number, integer, boolean, object, array, null).
    • The field is marked as optional in documentation but the schema lacks null in the type array.

    Correct patterns

    1. Use an array: "type": ["string","null"] for a field that may be a string or absent.
    2. When you need to allow multiple primitives, list them all: "type": ["number","integer"] (note that integer is a subset of number but explicitness helps readability).
    3. If you want to accept any value, use "type": [] (an empty array) or simply omit type and rely on other constraints; however, omitting type alone does not guarantee acceptance of all JSON values, so be explicit.
    4. Validate the schema itself with a meta‑validator (e.g., AJV’s validateSchema) before using it to validate data.

    Example of a nullable date field:

    {
      "type": "object",
      "properties": {
        "issuedAt": {
          "type": ["string","null"],
          "format": "date-time"
        }
      },
      "required": ["issuedAt"]
    }
    

    Here null is allowed, so {"issuedAt":null} passes, while {"issuedAt":"not-a-date"} fails.

    $ref traps and version drift

    A $ref that resolves to a missing location causes the whole schema to fail at load time, which is noisy and easy to spot. The quieter problem is a reference that resolves but points to the wrong definition because the draft version changed or the folder structure shifted. In Draft 2020-12 the keyword for reusable blocks moved from definitions to $defs. If you keep the old key, the schema still validates (the older keyword is ignored) but you lose the ability to reuse definitions, leading to duplicated code and drift.

    How to catch reference errors

    • Run the schema through a resolver that reports unresolved references; AJV v8 does this by default.
    • Check the $schema URI at the top of the file; it should match the draft you intend to use (https://json-schema.org/draft/2020-12/schema).
    • Search for definitions. If you are targeting Draft 2020-12, replace it with $defs and adjust the pointer accordingly.
    • Use relative URLs carefully; a reference like "$ref": "./common.json#/defs/user" breaks if the file moves.

    Best practices for $ref

    1. Always declare the draft with "$schema": "https://json-schema.org/draft/2020-12/schema" at the root.
    2. Place reusable pieces under $defs and reference them with "$ref": "#/$defs/person".
    3. If you need to support older consumers, keep a definitions block that mirrors $defs but treat it as deprecated.
    4. Validate the resolved reference by extracting the target schema and running it through a meta‑validator.
    5. When publishing a schema as part of an API contract, bundle all referenced files or use a URL that is versioned and immutable.

    Example of a correct reference:

    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$defs": {
        "address": {
          "type": "object",
          "properties": {
            "street": { "type": "string" },
            "city":   { "type": "string" }
          },
          "required": ["street","city"]
        }
      },
      "type": "object",
      "properties": {
        "billing": { "$ref": "#/$defs/address" },
        "shipping": { "$ref": "#/$defs/address" }
      },
      "required": ["billing","shipping"]
    }
    

    If you mistakenly wrote "$ref": "#/definitions/address" the reference would resolve to undefined and the validator would throw an error, alerting you to the mismatch.

    Validate with samples before publishing

    The cheapest confidence check is to run a handful of representative JSON instances through the validator before you treat the schema as a contract. A good test suite contains at least one valid example that matches the intended use and one invalid example that deliberately breaks each rule you care about. If the schema accepts the good sample and rejects the bad one, you have evidence that it is not overly permissive. I have seen teams skip this step and later discover that a missing required allowed orders without product IDs to slip through, causing fulfillment errors.

    Building a minimal test set

    • Good sample: copy a real payload from your service or fabricate one that satisfies every constraint.
    • Bad sample for each rule: remove a required field, add an unknown key, supply a wrong type, or point a $ref to a non‑existent definition.
    • If you use unions, test each branch and the null case when applicable.
    • Automate the check with a script that fails the build if any validation outcome is unexpected.

    Example validation script (Node.js)

    const Ajv = require('ajv');
    const ajv = new Ajv({allErrors: true});
    const schema = require('./user-schema.json');
    
    const good = { "id": "123", "name": "Ada" };
    const bad  = { "name": "Ada" }; // missing id
    
    const validate = ajv.compile(schema);
    console.log('Good sample:', validate(good) ? 'pass' : 'fail');
    if (!validate(good)) console.error(validate.errors);
    
    console.log('Bad sample:', validate(bad) ? 'pass' : 'fail');
    if (validate(bad)) console.error('Unexpectedly accepted bad sample');
    

    Running this in CI ensures that a permissive mistake never reaches production.

    What this is NOT

    This guide does not promise that following these points will make your schema immune to every possible mistake. JSON Schema is a flexible language, and new pitfalls can appear as you extend it with custom keywords or combine it with other tools like OpenAPI. I am not claiming that the four errors listed are the only ones that matter; they are simply the ones that appear most often in the code reviews I have seen. The advice here is based on my experience maintaining SchemaSafe and on publicly available specifications; it is not a substitute for reading the official specification or for running your own validation tests.

    References

    FAQ

    Why does my schema accept missing fields?

    Declaring a property in properties does not make it required. You must list it in the required array. The array is a sibling of properties, not nested inside it.

    What does additionalProperties do by default?

    It defaults to true, so unknown keys pass. Set additionalProperties: false where strictness matters, or constrain it with a schema for extension fields. Open schemas let typos through silently.

    How do I mark a field optional in JSON Schema?

    Omit it from required and include "null" in its type array, for example ["string", "null"]. Writing a comma-separated type string is invalid and can void the constraint.

    Where do reusable definitions go in Draft 2020-12?

    Under $defs. The older definitions key still works but marks an old draft. Always pin $schema so the validator applies the draft you intended.

    Try the product: SchemaSafe

    2026-09-16 · primary sources only · no fabricated traffic metrics