Skip to content
datahacks.dev
JSON

Required Fields and Optional Fields in JSON Schema

How required, null, empty strings and defaults differ in JSON Schema, with test results for dependentRequired and if-then rules for conditional fields.

6 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the JSON logo.

Almost every first schema gets "required" wrong in one of two ways: it lists a field under properties and believes that makes it mandatory, or it marks a field required and then finds that clients sending null or an empty string are accepted or rejected in ways nobody intended. The rules are small and precise. This article walks through them with the results of actual validations, then covers the cases where one field's presence depends on another.

Required lives on the parent object

The schema used for most of the tests:

Python
schema = {
    "type": "object",
    "required": ["name"],
    "properties": {
        "name": {"type": "string"},
        "nick": {"type": "string"},
        "bio":  {"type": ["string", "null"]},
    },
}

required is an array of key names, placed in the object schema that owns those keys. It is not a property of the field. Writing "name": {"type": "string", "required": true} is the draft 3 syntax and does nothing in any current draft; it is an unknown keyword and is ignored without a message. Everything not in the array is optional, and properties has no say in that.

The results for six documents:

Text
missing nick   OK
nick null      [$.nick] None is not of type 'string'
bio null       OK
name empty     OK
name null      [$.name] None is not of type 'string'
nick empty     OK

Reading them in turn. A document with no nick at all is fine, because nick is optional. A document with "nick": null is not fine: the key is present, so its schema applies, and null is not a string. An absent key and a null value are different things to JSON Schema, and many APIs treat them as the same. If your API does, say so in the schema by allowing null where you accept it, as bio does with "type": ["string", "null"]. Then {"name": "a", "bio": null} passes.

Required does not mean non-empty

"name": "" passed. It is present and it is a string. To forbid the empty string, add a length rule:

Python
{"type": "string", "minLength": 1}
# '' should be non-empty

The same holds for arrays (minItems: 1) and objects (minProperties: 1). In an input form, "required" almost always means "present and not blank", so the schema needs both parts: the name in required and a minLength of 1 (or a pattern such as \\S if whitespace counts as blank). A string of spaces passes minLength: 1, which is rarely what the form meant. A pattern of "\\S" rejects it, and trimming the input before validation is simpler still.

Missing versus null versus unknown

Three situations that look alike in a data table:

SituationExampleWhat the schema must say
Key missing{"name": "a"}Leave the key out of required
Key present, value null{"name": "a", "bio": null}Allow "null" in type
Key not in the schema{"name": "a", "x": 1}Passes unless additionalProperties is false

The last row catches people who rely on additionalProperties to detect typos. Without it, a client that sends nmae instead of name is rejected only because name is required. A client that mistypes an optional key, nik for nick, is accepted and its value is ignored, which is a silent failure. For input you control, close the object (additionalProperties: false, or unevaluatedProperties: false if you compose schemas with allOf). The cost is that new optional fields become breaking changes (versioning a JSON Schema without breaking consumers).

Defaults are documentation, not behavior

default states what a consumer should assume when a key is absent. A validator does not insert it. I validated an empty object against {"properties": {"role": {"type": "string", "default": "user"}}} and the object was still {} afterwards. The specification treats default as an annotation. If you want values filled in, do it in code, or use a library option designed for it. Ajv has useDefaults: true, which mutates the document during validation and filled plan: "free" into my test object. That is convenient and also the reason to be careful with it: validation that changes its input is easy to forget about, and a schema consumed by a validator without the option will not fill anything. The data-changing options in Ajv are covered in validating JSON in Node with Ajv.

Conditionally required fields

Real documents have rules like "if there is a card number, a security code is needed". Two keywords express them.

**dependentRequired** maps a key to the keys that must accompany it:

Python
{"type": "object",
 "properties": {"card": {"type": "string"}, "cvv": {"type": "string"}},
 "dependentRequired": {"card": ["cvv"]}}

{"card": "4111"} failed with [$] 'cvv' is a dependency of 'card', and {} passed, because no card means no dependency. In drafts before 2019-09 this was dependencies.

**if / then / else** handles conditions on values:

Python
{"type": "object",
 "required": ["kind"],
 "properties": {"kind": {"enum": ["person", "company"]}},
 "if":   {"properties": {"kind": {"const": "company"}}},
 "then": {"required": ["vat"]}}

{"kind": "company"} failed with [$] 'vat' is a required property, and {"kind": "person"} passed. A trap in the if: it must contain required as well if the condition should mean "the key exists and equals company". My if above matches any object that lacks kind, because properties is vacuously true for a missing key. Here that is harmless since kind is required at the top, but in other schemas it makes the then branch fire for documents that never mentioned the field. Write the condition as {"properties": {"kind": {"const": "company"}}, "required": ["kind"]} when kind is optional.

For more than two variants, oneOf with a discriminating const in each branch reads better than a chain of ifs, at the cost of error messages that list every branch. Test it by feeding a document that fits no variant and reading what the user would be shown.

Writing the schema to match reality

Start from the data you have, not from a wish. In the event data of the JSON Schema guide, amount was absent from no record but null in three quarters of them, and props.experiment was absent from 90%. The right schema marks amount as ["number", "null"] and not required-but-never-null, and leaves experiment out of required. A schema that says "required" for something the producer sometimes omits turns a normal day into an alert. Count first: jq -c 'keys' file.ndjson | sort | uniq -c shows which keys appear in every record and which do not, and a few lines of code can give you the percentage per key.

One more rule to adopt: **the schema's required list should be the contract, and the code should not depend on any key outside it.** If a consumer reads doc["nick"] without a guard, nick is required in practice and the schema is lying. Review the consumer code for every key access when you decide what goes in the list.

When required is the wrong keyword

You mean "must not be null". Absence and null are separate. Use type without null for non-null, and required for present. Both are needed for "present and not null".

The field is optional on input but always present on output. Write two schemas: one for requests, one for responses. A single schema that tries to be both ends up optional on both sides and checks neither.

Validation is incremental. A PATCH request legitimately omits most fields. Validate it against a schema without required, or the same schema applied to the merged document, not the partial one (comparing and patching JSON documents covers merge patches).

For the keywords' exact semantics, see required and conditional schemas in the official documentation, and JSON data types and what is missing from them for why null is its own case. Check a sample document in the JSON formatter.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.