JSON Schema additionalProperties Explained
How JSON Schema additionalProperties treats names a schema did not list, how that differs from unevaluatedProperties, and when closing an object breaks clients.
A properties map looks like the only names a JSON object may contain. In JSON Schema Draft 2020-12 it is not that list. A name the map never mentions stays valid unless some other keyword rejects it. For names in the same schema object, that keyword is additionalProperties. unevaluatedProperties is a different test, used once a name is declared in another branch. What an API schema constrains is the wider question. What a closed object does to old clients is a compatibility rule, not a default of the keyword.
What the keyword constrains
Section 10.3.2.3 says additionalProperties applies to instance names that are not in the annotation results of properties or patternProperties in the same schema object. Each leftover value is checked against the schema written as the keyword’s value.
false here is a boolean schema. Section 4.3.2 says the boolean schema false always fails validation. additionalProperties: false therefore fails an object that still has a leftover name. It does not fail an object whose every name was already covered.
A schema object as the value does not ban leftover names. It constrains their values. "additionalProperties": { "type": "string" } allows an extra name when the value is a string and rejects it when the value is not.
The object below is an illustration. The sku and the note are not a captured payload.
{ "sku": "item-17", "quantity": 2, "note": "rush" }
{
"type": "object",
"properties": {
"sku": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["sku", "quantity"],
"additionalProperties": false
}
Illustration. sku and quantity are covered by properties. note is not.
note is leftover, so false rejects the instance. The same schema accepts { "sku": "item-17", "quantity": 2 }, because nothing is left over. Replace false with { "type": "string" } and "rush" is valid. Set note to 1 and the name is still allowed, but the value does not validate against that subschema. Section 10.3.2.3 is the rule for which names are leftover and that those values must validate.
The usual mistake is to stop at properties and treat the map as closed.
Omitting the keyword
Section 10.3.2.3 says that omitting additionalProperties has the same assertion behavior as an empty schema. An empty schema does not reject the instance. Delete the additionalProperties line from the schema above and the object that carries note is valid. The names under properties did not change. A validator enforces keywords that are actually in the schema. It will not insert false for you.
Do not call that omission “the default is true.” Section 4.3.2 says the boolean schema true always passes, as the empty schema {} does, and that boolean schemas never produce annotation results. Section 7.3 says a missing keyword must not produce annotation results either, even when some present value would have the same assertion and would have produced annotations. Leftover names are not rejected. That is the assertion. The missing keyword still contributes no annotation. Writing true, writing {}, and leaving the keyword out are three different texts.
The annotation gap is visible when a parent uses unevaluatedProperties. This schema rejected a leftover name in the Ajv check:
{
"allOf": [
{
"type": "object",
"properties": {
"sku": { "type": "string" }
}
}
],
"unevaluatedProperties": false
}
Illustration. The branch lists sku and does not mention additionalProperties.
{ "sku": "item-17", "note": "rush" } failed unevaluatedProperties. { "sku": "item-17" } passed: sku is covered by properties. The same branch with "additionalProperties": {} written inside it accepted the leftover name. The empty schema is present, so it can annotate the names it validates. The omitted keyword is absent, so section 7.3 says it does not annotate them. unevaluatedProperties: false on the parent then still sees note.
properties, patternProperties, and the names left over
patternProperties is the other annotation section 10.3.2.3 consults, and only inside the same schema object. A name that matches a pattern is not leftover, even if properties never lists it.
{
"type": "object",
"properties": {
"sku": { "type": "string" }
},
"patternProperties": {
"^x-": { "type": "string" }
},
"additionalProperties": false,
"required": ["sku"]
}
Illustration. Names that start with x- are covered by the pattern, not by additionalProperties.
{ "sku": "item-17", "x-trace": "abc" } is valid. x-trace matches ^x-, and the value is a string. { "sku": "item-17", "note": "rush" } is not: note matches neither the map nor the pattern, and false rejects it. { "sku": "item-17", "x-trace": 4 } is not valid either. The name matched the pattern. The value did not match the pattern’s schema.
required demands that listed names be present. It does not reject a name that is absent from the list. Closing the set of names is a different job.
| Schema text | Instance { "sku": "item-17", "quantity": 2, "note": "rush" } |
Why |
|---|---|---|
additionalProperties omitted |
Valid | Section 10.3.2.3: omission has the assertion behavior of an empty schema |
additionalProperties: false |
Invalid | Section 4.3.2: boolean false fails the leftover value |
additionalProperties: { "type": "string" } |
Valid | The leftover value matches the subschema. A number in note would not |
The instance is the illustration above, with sku and quantity covered by properties. “Valid” and “Invalid” are the assertion results those sections require. Ajv 8.20.0 agreed on 5 November 2026. The table is not a library's error message, and it is not a count of client failures.
additionalProperties and unevaluatedProperties
Section 11.3 applies unevaluatedProperties to names that do not appear in the annotation results of properties, patternProperties, additionalProperties, or unevaluatedProperties. Those annotations may come from adjacent in-place applicators, not only from keywords written beside unevaluatedProperties. additionalProperties does not use that wider set. Setting both keywords to false therefore does not reject the same names once a schema is split.
Names declared in another branch
One instance, { "sku": "item-17", "quantity": 2 }. Two parent schemas. Both require sku inside allOf and quantity on the parent. Only the closing keyword changes. With additionalProperties: false on the parent, the Ajv check rejected the instance. quantity sits beside that keyword, so it is covered. sku is declared in the other branch. It is not a sibling properties entry, and section 10.3.2.3 does not look there.
{
"allOf": [
{
"type": "object",
"properties": { "sku": { "type": "string" } },
"required": ["sku"]
}
],
"properties": { "quantity": { "type": "integer" } },
"required": ["quantity"],
"unevaluatedProperties": false
}
Illustration. The same shape with additionalProperties: false in place of unevaluatedProperties rejected sku.
unevaluatedProperties: false accepted sku and quantity. Section 11.3 collects names the allOf branch evaluated. Adding "note": "rush" made this schema fail. Nothing had evaluated note.
The same check accepted a name brought in by if/then, and a name brought in by $ref, when the parent used unevaluatedProperties: false. Replacing that keyword with additionalProperties: false on the if/then schema rejected the name that existed only in then. Those two runs are the same rule as the allOf case. They are not a promise about every applicator a later draft might add.
The opposite mistake is a branch that is already closed. additionalProperties: false inside the allOf schema that only knows sku rejects quantity when that name exists only on the parent. unevaluatedProperties on the parent does not reopen a branch that already failed. Repeat the base names next to the new false, or leave the branch open and close the parent with unevaluatedProperties.
OpenAPI 3.1.0 says a Schema Object is a superset of Draft 2020-12 and that, unless it states otherwise, those properties follow JSON Schema. The properties it says it extends are description and format, not additionalProperties. Which file you are holding separates that dialect from a bare JSON Schema document. This note does not claim OpenAPI 3.1 changes the keyword.
Closing an object is a compatibility rule
additionalProperties: false is a compatibility decision written as a keyword. Adding a response field stays wire-compatible for a client that ignores unknown names. It is a break for a client whose schema uses false: the new name is leftover, and the boolean schema fails.
If the contract says clients ignore unknown response fields, do not publish a client schema that sets false on those objects. If the contract says the object is closed, the schema should say false in the schema object that owns the list. Validation enforces the file, not a wiki page that says the opposite.
Whether a closed list belongs on a request or a response is this desk’s reading, not a rule in section 10.3.2.3.
A checklist before additionalProperties is set to false
Use this before a schema is treated as closed. It is a review list, not a score.
- Every name that must be accepted is in
propertiesorpatternPropertiesin the same schema object as thefalse, or the schema usesunevaluatedPropertiesand those names are actually evaluated where that keyword can see them. - A name that lives only inside
allOf,if/then, or$refis not assumed to satisfy a siblingadditionalProperties: false. - An inner schema does not already set
additionalProperties: falseif a later schema is supposed to add a name. - Response objects that are allowed to grow do not set
false. The compatibility note and the keyword match. - Request objects that must reject unknown names set
falseon purpose.propertiesalone does not do it. - The file is Draft 2020-12, or an OpenAPI 3.1 Schema Object. Another draft is not assumed to mean the same thing.
- The check you run uses the file clients will use. Where that check sits is a separate decision.
Open questions
Section 4.3.2 says boolean schemas never produce annotation results, and also says true validates as the empty schema {} does. In the Ajv 8.20.0 check, a branch with additionalProperties: true and a parent unevaluatedProperties: false accepted a leftover name, the same result as a branch that used {}. This note does not turn that library result into a requirement the boolean-schema paragraph did not write. If the annotation matters, write the schema object you mean and test the validator you ship.
OpenAPI 3.0 is out of scope. Its Schema Object is not Draft 2020-12. Section 10.3.2.3 does not standardize a validator’s failure sentence either.
Sources
- JSON Schema Core, Draft 2020-12, section 4.3.2 (boolean schemas) — JSON Schema
- JSON Schema Core, Draft 2020-12, section 7.3 (default behaviors) — JSON Schema
- JSON Schema Core, Draft 2020-12, section 10.3.2.3 (additionalProperties) — JSON Schema
- JSON Schema Core, Draft 2020-12, section 11.3 (unevaluatedProperties) — JSON Schema
- OpenAPI Specification 3.1.0, Schema Object — OpenAPI Initiative
- Ajv 8.20.0 — Ajv