JSON Schema additionalProperties Explained

Last updated:

additionalProperties is the JSON Schema keyword that controls what happens to object keys not covered by the properties or patternProperties keywords. Set it to false and any unrecognized key makes the object invalid. Set it to a schema and every extra key must satisfy that schema. Leave it out and extra keys are silently permitted. Understanding this keyword — and its more powerful successor unevaluatedProperties — is essential for building strict, correct JSON Schema validation.

Default Behavior: additionalProperties Not Set

When additionalProperties is absent, JSON Schema uses the permissive default: any additional key is allowed, regardless of name or value. This is the open-world assumption — objects are free to carry extra data beyond what the schema declares.

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

// Valid — 'extra' key is silently permitted
{ "name": "Alice", "age": 30, "extra": "anything" }

// Also valid — no additionalProperties constraint
{ "name": "Bob", "unknown_field": 12345, "foo": null }

This default is intentional: JSON Schema is designed to be composable. Restricting extra properties would break schema reuse patterns where a base schema is extended. If you want a closed object, you must opt in explicitly.

Reject All Unknown Properties (false)

Setting additionalProperties: false turns the schema into a closed object. Every key in the instance must appear in properties (or match a pattern in patternProperties). Any other key causes validation failure.

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

// Valid
{ "id": 1, "email": "alice@example.com" }

// Invalid — 'role' is not in 'properties'
{ "id": 1, "email": "alice@example.com", "role": "admin" }
// Error: must NOT have additional properties

Note that required lists which properties are mandatory, but it does not restrict additional properties. Only additionalProperties: false does that. The two keywords are orthogonal.

In Ajv, you can also strip extra properties instead of failing:

import Ajv from 'ajv'

// Fail on additional properties (default)
const ajvStrict = new Ajv()

// Remove additional properties silently
const ajvStrip = new Ajv({ removeAdditional: true })

// Remove only properties that fail the additionalProperties schema
const ajvFailing = new Ajv({ removeAdditional: 'failing' })

const schema = {
  type: 'object',
  properties: { name: { type: 'string' } },
  additionalProperties: false,
}

const data = { name: 'Alice', extra: 'value' }

ajvStrict.validate(schema, data)  // false — validation fails
ajvStrip.validate(schema, data)   // true — 'extra' removed from data
console.log(data)                 // { name: 'Alice' }

Validate Extra Properties Against a Schema

Instead of false, you can supply a schema as the value of additionalProperties. Every key not covered by properties or patternProperties must satisfy that schema. This is useful for extensible objects that allow metadata of a known type.

{
  "type": "object",
  "properties": {
    "id":    { "type": "integer" },
    "title": { "type": "string" }
  },
  "additionalProperties": { "type": "string" }
}

// Valid — extra keys 'author' and 'tag' are strings
{ "id": 1, "title": "Post", "author": "Alice", "tag": "json" }

// Invalid — 'views' is a number, not a string
{ "id": 1, "title": "Post", "views": 1000 }
// Error: additionalProperties 'views' must be string

More complex schemas work too. A common pattern is allowing extra keys with any value except null:

{
  "type": "object",
  "properties": {
    "name": { "type": "string" }
  },
  "additionalProperties": { "not": { "type": "null" } }
}

// Valid — extra key 'score' is not null
{ "name": "Alice", "score": 42 }

// Invalid — 'score' is null
{ "name": "Alice", "score": null }

patternProperties vs additionalProperties

patternProperties maps regular expressions to schemas. Any key whose name matches the regex must satisfy the associated schema. A key can match multiple patterns simultaneously — it must satisfy all of them. patternProperties and additionalProperties are applied in combination: a key is considered "covered" if it appears in properties (exact match) or matches at least one pattern in patternProperties.

{
  "type": "object",
  "properties": {
    "id": { "type": "integer" }
  },
  "patternProperties": {
    "^str_": { "type": "string" },
    "^num_": { "type": "number" }
  },
  "additionalProperties": false
}

// Valid — 'id' is in properties; 'str_name' matches '^str_'; 'num_price' matches '^num_'
{ "id": 1, "str_name": "Alice", "num_price": 9.99 }

// Invalid — 'extra' does not match 'id', '^str_', or '^num_'
{ "id": 1, "extra": "rejected" }

// Invalid — 'str_count' matches '^str_' but the value is not a string
{ "id": 1, "str_count": 42 }

A practical use case: allow any string-valued metadata key prefixed with x- (like HTTP extension headers) while keeping core properties strictly typed:

{
  "type": "object",
  "properties": {
    "name":  { "type": "string" },
    "email": { "type": "string", "format": "email" }
  },
  "patternProperties": {
    "^x-": { "type": "string" }
  },
  "additionalProperties": false,
  "required": ["name", "email"]
}

// Valid
{ "name": "Alice", "email": "a@example.com", "x-source": "crm", "x-tenant": "acme" }

// Invalid — 'role' is not in properties and doesn't match '^x-'
{ "name": "Alice", "email": "a@example.com", "role": "admin" }

unevaluatedProperties (Draft 2019-09+)

unevaluatedProperties was introduced in JSON Schema Draft 2019-09 to solve the fundamental limitation of additionalProperties: it cannot see through allOf, anyOf, oneOf, or if/then/else. A property is "unevaluated" if no keyword in the entire schema tree has evaluated it — including subschemas reached via $ref or composition keywords.

// Problem: additionalProperties cannot see through allOf
// This schema will INCORRECTLY reject 'name' from the $ref'd subschema
{
  "type": "object",
  "allOf": [{ "$ref": "#/$defs/HasName" }],
  "properties": { "age": { "type": "integer" } },
  "additionalProperties": false,   // ← Does NOT see 'name' from the $ref
  "$defs": {
    "HasName": {
      "properties": { "name": { "type": "string" } }
    }
  }
}
// { "name": "Alice", "age": 30 } → FAILS (incorrectly)

// Solution: use unevaluatedProperties instead
{
  "type": "object",
  "allOf": [{ "$ref": "#/$defs/HasName" }],
  "properties": { "age": { "type": "integer" } },
  "unevaluatedProperties": false,   // ← Sees 'name' evaluated by allOf/$ref
  "$defs": {
    "HasName": {
      "properties": { "name": { "type": "string" } }
    }
  }
}
// { "name": "Alice", "age": 30 } → PASSES (correctly)
// { "name": "Alice", "age": 30, "role": "admin" } → FAILS (correctly)

To use unevaluatedProperties with Ajv, import from ajv/dist/2020:

import Ajv2020 from 'ajv/dist/2020'
const ajv = new Ajv2020()

const schema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  type: 'object',
  allOf: [{ $ref: '#/$defs/Base' }],
  properties: { role: { type: 'string' } },
  unevaluatedProperties: false,
  $defs: {
    Base: {
      properties: {
        id:   { type: 'integer' },
        name: { type: 'string' },
      },
    },
  },
}

const validate = ajv.compile(schema)

validate({ id: 1, name: 'Alice', role: 'admin' })  // true
validate({ id: 1, name: 'Alice', extra: 'x' })     // false — 'extra' is unevaluated

Combining with allOf and Inheritance Patterns

A common pattern in API design is to define a base object schema and extend it for specific resource types. Getting additionalProperties right in these compositions is one of the most frequent sources of confusion.

// Pattern 1: Workaround for Draft-07 — repeat properties at the top level
// (compatible with all validators, but verbose)
{
  "$defs": {
    "Base": {
      "type": "object",
      "properties": {
        "id":        { "type": "integer" },
        "createdAt": { "type": "string", "format": "date-time" }
      },
      "required": ["id", "createdAt"]
    }
  },
  "allOf": [{ "$ref": "#/$defs/Base" }],
  "properties": {
    "id":        {},              // Repeated from Base so additionalProperties sees it
    "createdAt": {},             // Repeated from Base
    "name":      { "type": "string" }
  },
  "required": ["name"],
  "additionalProperties": false
}

// Pattern 2: Use unevaluatedProperties (Draft 2019-09+, cleaner)
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "Base": {
      "type": "object",
      "properties": {
        "id":        { "type": "integer" },
        "createdAt": { "type": "string", "format": "date-time" }
      },
      "required": ["id", "createdAt"]
    }
  },
  "allOf": [{ "$ref": "#/$defs/Base" }],
  "properties": {
    "name": { "type": "string" }
  },
  "required": ["name"],
  "unevaluatedProperties": false
}

// Pattern 3: Composition via if/then — unevaluatedProperties required
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": { "type": { "type": "string" } },
  "if":   { "properties": { "type": { "const": "user" } } },
  "then": { "properties": { "email": { "type": "string" } } },
  "else": { "properties": { "code": { "type": "string" } } },
  "unevaluatedProperties": false
}

When sharing schemas across teams, prefer keeping each schema open (no additionalProperties: false) and applying the restriction only at the outermost consumer schema. This preserves composability and makes schema evolution easier.

Ajv Configuration and Common Errors

Here are the most common mistakes and their fixes when using additionalProperties with Ajv:

import Ajv from 'ajv'
import addFormats from 'ajv-formats'

const ajv = new Ajv({ allErrors: true })
addFormats(ajv)

// ── Mistake 1: additionalProperties: false with $ref / allOf ─────────────────
// Symptom: Valid objects fail because inherited properties are rejected
// Fix: Use unevaluatedProperties with Ajv2020, or repeat property names

import Ajv2020 from 'ajv/dist/2020'
const ajv2020 = new Ajv2020({ allErrors: true })

// ── Mistake 2: Forgetting that 'required' ≠ 'additionalProperties' ───────────
// These are orthogonal: required lists mandatory keys;
// additionalProperties restricts unknown keys.
const schema1 = {
  type: 'object',
  properties:           { name: { type: 'string' } },
  required:             ['name'],
  additionalProperties: false,
}
// { name: 'Alice' }            → valid
// { name: 'Alice', x: 1 }     → INVALID (x is additional)
// {}                           → INVALID (name is required)

// ── Mistake 3: additionalProperties on nested objects ────────────────────────
// additionalProperties only applies to the schema object where it appears.
// Nested objects need their own additionalProperties.
const schema2 = {
  type: 'object',
  properties: {
    user: {
      type: 'object',
      properties: { name: { type: 'string' } },
      additionalProperties: false,   // controls keys of 'user', not the root
    },
  },
  additionalProperties: false,       // controls keys of the root object
}

// ── Mistake 4: Expecting additionalProperties to validate key names ───────────
// additionalProperties validates VALUES of extra keys, not the key names.
// Use propertyNames to constrain key names:
const schema3 = {
  type: 'object',
  propertyNames: { pattern: '^[a-z_]+$' },   // key names must be lowercase + underscore
  additionalProperties: { type: 'string' },   // values must be strings
}

Definitions

additionalProperties
A JSON Schema keyword applied to objects. When set to false, rejects any key not declared in properties or matched by patternProperties in the same schema object. When set to a schema, validates the values of those uncovered keys against that schema.
patternProperties
A JSON Schema keyword that maps regular expressions to schemas. Any object key whose name matches a regex must satisfy the corresponding schema. Works additively with properties (exact-name matching) and jointly determines which keys are "covered" for the purposes of additionalProperties.
unevaluatedProperties
Introduced in JSON Schema Draft 2019-09. Like additionalProperties but aware of the full schema evaluation tree, including properties evaluated by allOf, anyOf, oneOf, if/then/else, and $ref. Use this instead of additionalProperties: false when composing schemas.
Evaluated property
A property that has been "touched" by a schema keyword during validation. In the context of unevaluatedProperties, a property is evaluated if any schema in the evaluation path (including subschemas) declares it in properties, matches it in patternProperties, or reaches it through composition keywords.
JSON Schema Draft 2020-12
The latest stable version of the JSON Schema specification (published December 2020). Includes unevaluatedProperties, prefixItems, and other improvements over earlier drafts. Identified by {"$schema: 'https://json-schema.org/draft/2020-12/schema'"}. Supported by Ajv 8+ via ajv/dist/2020.

FAQ

What does additionalProperties: false do in JSON Schema?

It causes validation to fail if the object contains any key not listed in properties or matched by patternProperties in the same schema object. It is a strict closed-object constraint. Any extra key — regardless of its value — makes the instance invalid. This is the primary mechanism for enforcing exact object shapes in JSON Schema.

What is the default behavior when additionalProperties is omitted?

The default is true, equivalent to the empty schema {}. Any additional property is permitted with any value. JSON Schema is open by default — you must explicitly opt in to closed-object behavior with additionalProperties: false. This design choice supports composability: schemas can safely be combined without accidentally blocking each other's properties.

Can additionalProperties validate extra keys against a schema instead of rejecting them?

Yes. Set additionalProperties to a schema object instead of false. Every key not covered by properties or patternProperties must satisfy that schema. For example, {"additionalProperties": {"type": "string"}} allows any extra key as long as its value is a string. Only values are validated — the key names are not constrained by additionalProperties (use propertyNames for that).

How does patternProperties interact with additionalProperties?

patternProperties and properties jointly determine which keys are "covered." When additionalProperties: false is present, a key must either match an exact name in properties or match at least one regex in patternProperties. Any key that matches neither is treated as an additional property and rejected. The two keywords are additive — a key can satisfy both simultaneously.

What is unevaluatedProperties and how does it differ from additionalProperties?

additionalProperties only sees properties and patternProperties in the same schema object — it cannot look inside allOf, $ref, or if/then. unevaluatedProperties (Draft 2019-09+) considers all properties evaluated anywhere in the schema tree. Use unevaluatedProperties when composing schemas with allOf or $refadditionalProperties: false in those contexts will incorrectly reject inherited properties.

Why does additionalProperties: false break when used with allOf?

Because additionalProperties only inspects properties in the same schema object. Properties declared inside allOf subschemas are invisible to it, so they are treated as additional and rejected. The two common fixes are: (1) duplicate the inherited property names into properties at the same level as additionalProperties (Draft-07 compatible, but verbose), or (2) switch to unevaluatedProperties: false with a Draft 2019-09+ validator like Ajv 8+.

How do I configure Ajv to enforce additionalProperties: false?

No special Ajv configuration is needed — additionalProperties: false is enforced by default. To strip extra properties instead of failing, pass removeAdditional: true to the Ajv constructor. For unevaluatedProperties support (Draft 2019-09+), import Ajv2020 from ajv/dist/2020 instead of the default export. Install ajv version 8 or later for full 2020-12 support.

What is the difference between additionalProperties and unevaluatedProperties in Draft 2020-12?

In Draft 2020-12, additionalProperties still only sees properties and patternProperties in the same schema object. unevaluatedProperties sees every property evaluated by the entire schema instance — including those reached via $ref, allOf, anyOf, oneOf, and if/then/else. Use additionalProperties for flat schemas with no composition. Use unevaluatedProperties for composed schemas where inherited properties must also be recognized.

Further reading and primary sources