JSON Schema subset
atlas-json-schema-subset-v1 — every keyword accepted, every keyword refused, and every violation code.
Structured output schemas are validated against a published, versioned
subset of JSON Schema. Its identifier is atlas-json-schema-subset-v1, and
each model reports the subset it uses as capabilities.json_schema_subset on
GET /v1/models.
The subset is published rather than discovered so you can validate a schema in your own build, before it ever reaches Atlas. A schema outside it is refused at request validation — never answered with unvalidated content.
Accepted keywords
Structural
type · properties · required · additionalProperties · items ·
enum · const · anyOf · $defs · $ref
Annotations
title · description · examples · default · $comment · $schema ·
$id
Annotations are carried and never enforced. They are safe precisely because
they constrain nothing — default is accepted for that reason, so a schema
emitted by a zod .default() is not rejected.
Accepted types
object · array · string · number · integer · boolean · null
Refused keywords
Refused by name, so the error says pattern is not in the subset — a
sentence you can act on — rather than "unknown keyword", which reads like an
Atlas bug.
| Group | Keywords |
|---|---|
| Strings | format, pattern, minLength, maxLength |
| Numbers | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
| Objects | patternProperties, propertyNames, minProperties, maxProperties, unevaluatedProperties, dependentRequired, dependentSchemas |
| Arrays | prefixItems, contains, minContains, maxContains, minItems, maxItems, uniqueItems, unevaluatedItems |
| Composition | allOf, oneOf, not, if, then, else |
anyOf is the only composition keyword in the subset. Anything else in that
group has to be restructured — usually into an anyOf of fully-specified
object branches.
Any keyword that is neither accepted nor listed above is also refused; the two lists are not the same thing, and the error message tells you which case you hit.
Structural rules
Root must be an object schemaroot_not_objectThe document's root type must be object.
Every property is requiredoptional_propertyEvery key in properties must appear in required. Model optionality as a
type union including null, and require the property.
Objects must closeadditional_propertiesEvery object schema must set additionalProperties: false — at every level,
not only the root.
Maximum nesting depth is 5depth_limitCounted from the root.
References must be local and resolvableunresolvable_refA $ref must be #/$defs/<name> and must name a def that exists in the same
document.
Violation codes
Every refusal carries a code from a closed set, so you can branch on it:
| Code | Meaning |
|---|---|
root_not_object | The document is not an object schema at its root. |
unsupported_keyword | A keyword the subset does not accept. |
optional_property | A property that required does not list. |
additional_properties | An object that does not forbid additional properties. |
depth_limit | Nesting past 5 levels. |
unsupported_type | A type outside the accepted list. |
unresolvable_ref | A $ref that is not a local #/$defs/<name>, or names no such def. |
malformed_schema | Structurally not a schema: a non-object node, a missing type, a bad items. |
Each violation also carries an RFC 6901 pointer into your schema document
("" for the root), with ~0 and ~1 escaping applied, so a property named
a/b cannot forge a pointer segment.
Working within it
{
"type": "object",
"properties": {
"nickname": { "type": ["string", "null"] }
},
"required": ["nickname"],
"additionalProperties": false
}The field is always present; its absence is expressed as null.
Use enum or const rather than pattern:
{ "type": "string", "enum": ["draft", "published", "retired"] }Use anyOf over fully-specified object branches, each pinned by a const
discriminator:
{
"anyOf": [
{
"type": "object",
"properties": { "kind": { "const": "email" }, "address": { "type": "string" } },
"required": ["kind", "address"],
"additionalProperties": false
},
{
"type": "object",
"properties": { "kind": { "const": "phone" }, "number": { "type": "string" } },
"required": ["kind", "number"],
"additionalProperties": false
}
]
}Not expressible in the subset. Ask for the bound in the prompt, and enforce it in your own code after parsing. Schema-constrained decoding gives you shape guarantees, not range guarantees.
Versioning
The identifier atlas-json-schema-subset-v1 is part of the published catalog.
If the accepted set changes in a way that would reject a schema this version
accepts, it is published under a new identifier — the same rule model
identifiers follow. Read capabilities.json_schema_subset from the catalog
rather than hard-coding the string.