JSON Schema Annotations: title, description, examples, default
Last updated:
JSON Schema annotations are keywords that carry metadata about a schema or property without influencing whether data passes validation. They exist to communicate intent — to developers reading raw JSON Schema files, to documentation generators like Swagger UI and Redoc, to form builders that auto-generate UI, and to SDK generators that produce client code. Getting annotations right pays dividends across every tool in the API lifecycle.
This guide covers every annotation keyword in detail: title and description for documentation, default for default values and how Ajv handles them, examples for showing valid inputs, readOnly and writeOnly for access control semantics, deprecated for migration signaling, and how OpenAPI 3.1 consumes all of these.
Want to validate JSON against a schema right now? Jsonic's JSON Schema Validator uses Ajv under the hood and shows exact error paths when validation fails.
Validate JSON Schema in Jsonictitle and description Keywords
title provides a short human-readable label for a schema or property. description provides a longer explanation — purpose, constraints, edge cases, and usage notes. Both are string values and both are pure annotations: removing them has no effect on validation.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User",
"description": "A registered user account. Email is the login identifier.",
"type": "object",
"properties": {
"id": {
"title": "User ID",
"description": "UUID v4 assigned by the server on account creation.",
"type": "string",
"format": "uuid",
"readOnly": true
},
"email": {
"title": "Email Address",
"description": "Primary login email. Must be unique across all accounts.",
"type": "string",
"format": "email",
"maxLength": 254
},
"displayName": {
"title": "Display Name",
"description": "Publicly visible name shown in the UI. 2–50 characters.",
"type": "string",
"minLength": 2,
"maxLength": 50
}
},
"required": ["email", "displayName"]
}Swagger UI and Redoc use title as the field label in rendered documentation. When title is absent, tools fall back to the property key name. The description appears as help text or a tooltip. Writing good descriptions pays forward — a well-annotated schema makes generated documentation indistinguishable from hand-written API docs.
Both keywords apply at every level of the schema: the root object, individual properties, array items, and $defs entries. Annotating $defs entries is especially valuable because those definitions are reused across multiple endpoints, and the description travels with every $ref that references them.
| Keyword | Type | Purpose | Tool usage |
|---|---|---|---|
title | string | Short label for the schema or field | Field label in Swagger UI, Redoc, form builders |
description | string | Longer explanation of purpose and constraints | Help text / tooltip in docs and generated forms |
default | any | Value used when the field is absent | Pre-filled value in Try-it-out forms; Ajv populates with useDefaults |
examples | array | Array of valid example values | Example dropdown in Swagger UI; test data for generated clients |
readOnly | boolean | Field is managed by server; not writable in requests | Omit from request body in generated docs |
writeOnly | boolean | Field accepted in requests but never returned in responses | Omit from response examples; render as password input |
deprecated | boolean | Field is obsolete; avoid using it | Strike-through in Swagger UI; deprecation warning in SDK generators |
default: Specifying Default Values
The default keyword declares what value should be assumed when the property is absent from the data. It is an annotation — a JSON Schema validator does not fail data that omits a property with a default, nor does it automatically write the default into the data object. The value is informational only, unless you explicitly configure the validator to apply it.
{
"type": "object",
"properties": {
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"],
"default": "viewer",
"description": "Access role. Defaults to read-only viewer if not specified."
},
"active": {
"type": "boolean",
"default": true
},
"pageSize": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 20
}
}
}To actually populate defaults during Ajv validation, pass { useDefaults: true } to the Ajv constructor. Ajv then writes the default value into the data object for any missing property — mutating the input in place:
import Ajv from 'ajv'
const ajv = new Ajv({ useDefaults: true })
const schema = {
type: 'object',
properties: {
role: { type: 'string', default: 'viewer' },
active: { type: 'boolean', default: true },
pageSize: { type: 'integer', default: 20 },
},
}
const validate = ajv.compile(schema)
const data = { role: 'editor' } // active and pageSize are absent
validate(data)
console.log(data)
// { role: 'editor', active: true, pageSize: 20 }
// Ajv wrote the defaults into the objectImportant caveats with useDefaults: it only applies defaults for missing object properties, not missing array items. It mutates the original object, so clone your input first if you need to preserve it. Defaults are applied depth-first — nested object defaults are filled before the parent is validated. If a property is present but set to null, Ajv does not replace it with the default.
examples: Showing Valid Values
The examples keyword (introduced in Draft-07) holds an array of values that are valid against the schema. It is distinct from default: a default is what the system uses when the field is absent; examples are illustrative inputs for documentation. All example values should be valid against the schema they annotate, though validators do not verify this.
{
"type": "object",
"title": "Create Order Request",
"properties": {
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "ISO 4217 currency code.",
"examples": ["USD", "EUR", "GBP", "JPY"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["sku", "quantity"]
},
"examples": [
[{ "sku": "WIDGET-001", "quantity": 2 }],
[{ "sku": "WIDGET-001", "quantity": 1 }, { "sku": "GADGET-007", "quantity": 3 }]
]
}
},
"required": ["currency", "items"],
"examples": [
{
"currency": "USD",
"items": [{ "sku": "WIDGET-001", "quantity": 2 }]
}
]
}Note that examples at the root of the schema shows a complete valid request body, while examples on individual properties shows valid values for that field. Swagger UI renders the first entry from examples as the default example in the Try-it-out form.
OpenAPI 2.0 and 3.0 use example (singular string, not an array) on the Schema Object — this is a Swagger extension that predates JSON Schema's examplesarray. In OpenAPI 3.1 (aligned with JSON Schema Draft 2020-12), examples (the JSON Schema array) is preferred. When migrating from OpenAPI 3.0 to 3.1, replace example: "USD" with examples: ["USD"].
readOnly and writeOnly
readOnly: true marks a property that is returned by the server but must not be sent by the client. writeOnly: true marks a property that can be sent by the client but is never returned by the server. Both are boolean annotations — validators do not enforce them at validation time.
{
"title": "User",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"readOnly": true,
"description": "Server-assigned unique identifier. Do not include in POST/PUT requests."
},
"createdAt": {
"type": "string",
"format": "date-time",
"readOnly": true,
"description": "Account creation timestamp. Set by the server."
},
"email": {
"type": "string",
"format": "email"
},
"password": {
"type": "string",
"minLength": 12,
"writeOnly": true,
"description": "Account password. Accepted in registration and password-change requests. Never returned."
},
"displayName": {
"type": "string",
"minLength": 2,
"maxLength": 50
}
},
"required": ["email", "displayName"]
}The practical effect of these annotations depends entirely on the tooling consuming the schema. OpenAPI generators use them as follows: readOnly properties are excluded from generated request body schemas and pre-filled request examples. writeOnly properties are excluded from generated response schemas and response examples. SDK generators like openapi-typescript and openapi-generator mark readOnly properties as readonly in TypeScript interfaces and read-only in Java/C# classes.
If you want a validator to actually reject a readOnly field in a request body, you need to use a different schema for requests vs responses — or write a custom keyword. A common pattern in OpenAPI is to define two schemas: a UserInput schema (no id, no createdAt) for request bodies, and a UserResponse schema (all fields) for response bodies.
deprecated Keyword
deprecated: true signals that a property or schema is obsolete and should no longer be used, even though it is still present for backward compatibility. This keyword was introduced in Draft 2019-09. Like all annotations, it has no effect on validation.
{
"type": "object",
"properties": {
"username": {
"type": "string",
"deprecated": true,
"description": "Deprecated since v2. Use 'email' as the login identifier instead.",
"x-deprecatedSince": "2025-01-01",
"x-replacement": "email"
},
"email": {
"type": "string",
"format": "email",
"description": "Primary login identifier as of v2."
},
"phone": {
"type": "string",
"deprecated": true,
"description": "Deprecated. Use the separate /phone-verify endpoint to manage phone numbers."
}
}
}Swagger UI renders deprecated fields with a strike-through style. Code generators emit deprecation warnings or @Deprecated / @deprecated annotations in the generated code. The deprecated keyword is most useful when you annotate $defs entries — tools that dereference $ref will see the deprecation annotation wherever the definition is used.
Best practice: combine deprecated: true with a description that explains what to use instead and, optionally, a custom extension field (prefixed with x-) such as x-deprecatedSince or x-replacement. JSON Schema ignores unknown keywords unless you're in Ajv strict mode, so custom extensions are safe in Draft 2019-09+ schemas.
Annotations in OpenAPI 3.1
OpenAPI 3.1 is the first version fully aligned with JSON Schema (Draft 2020-12). All JSON Schema annotation keywords are first-class citizens and are consumed by the entire OpenAPI toolchain.
# openapi: 3.1.0 — Schema Object with full annotation support
components:
schemas:
Product:
title: Product
description: |
A catalog product. Prices are in the currency specified at the account level.
Inventory is updated asynchronously and may lag by up to 60 seconds.
type: object
properties:
id:
title: Product ID
type: string
format: uuid
readOnly: true
examples:
- "550e8400-e29b-41d4-a716-446655440000"
name:
title: Product Name
type: string
minLength: 1
maxLength: 255
examples:
- "Wireless Ergonomic Mouse"
- "USB-C Hub 7-in-1"
price:
title: Price (USD cents)
type: integer
minimum: 0
description: Price in the smallest currency unit (cents for USD).
default: 0
examples:
- 2999
- 14999
sku:
title: SKU
type: string
pattern: "^[A-Z]{3}-[0-9]{6}$"
deprecated: true
description: "Deprecated in v3. Use 'productCode' instead."
examples:
- "WID-001234"
productCode:
title: Product Code
type: string
description: "Replaces 'sku' as of API v3. Format: CATEGORY-XXXXXX."
examples:
- "MOUSE-001234"
required:
- name
- priceIn OpenAPI 3.1, examples on a Schema Object is the JSON Schema array (as shown above). The OpenAPI-specific examples map (in the Media Type Object) is a separate construct for providing named, full request/response body examples — these are different and serve different purposes. The schema-level examples provides per-field illustration; the media-type-level examples provide complete payload examples for the Try-it-out interface.
Collecting Annotations with Ajv
Ajv does not collect or report annotation values by default. Its primary purpose is validation — determining pass or fail — rather than metadata extraction. To work with annotation values programmatically, you have two main approaches.
The first is the useDefaults option, which is Ajv's built-in annotation behavior — it writes default values into the validated data object:
import Ajv from 'ajv'
// useDefaults: true — the only annotation Ajv acts on automatically
const ajv = new Ajv({ useDefaults: true, allErrors: true })
const schema = {
type: 'object',
properties: {
theme: { type: 'string', enum: ['light', 'dark'], default: 'light' },
language: { type: 'string', default: 'en' },
timezone: { type: 'string', default: 'UTC' },
notifications: {
type: 'object',
properties: {
email: { type: 'boolean', default: true },
push: { type: 'boolean', default: false },
},
},
},
}
const validate = ajv.compile(schema)
// User only specified theme; all other properties are missing
const userPrefs = { theme: 'dark' }
validate(userPrefs)
console.log(userPrefs)
// {
// theme: 'dark',
// language: 'en',
// timezone: 'UTC',
// notifications: { email: true, push: false }
// }
// Nested defaults are also appliedThe second approach is to traverse the schema directly to extract any annotation value. Use json-schema-traverse or a recursive walk to collect all title, description, examples, or deprecated values alongside their JSON paths:
// Minimal annotation extractor — no dependencies required
function extractAnnotations(schema, path = '') {
const result = []
const annotations = ['title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly']
const found = {}
for (const key of annotations) {
if (key in schema) found[key] = schema[key]
}
if (Object.keys(found).length > 0) {
result.push({ path, ...found })
}
if (schema.properties) {
for (const [prop, subSchema] of Object.entries(schema.properties)) {
result.push(...extractAnnotations(subSchema, `${path}/${prop}`))
}
}
if (schema.items && typeof schema.items === 'object') {
result.push(...extractAnnotations(schema.items, `${path}/items`))
}
return result
}
const annotations = extractAnnotations(schema)
// [
// { path: '/email', title: 'Email Address', description: '...' },
// { path: '/id', title: 'User ID', readOnly: true },
// ...
// ]For production use, prefer a library like json-schema-traverse (which handles all subschema locations including allOf, anyOf, $defs, and if/then/else) rather than the manual recursive walk above, which only covers flat properties and items.
Definitions
- annotation
- A JSON Schema keyword that attaches metadata to a schema without affecting validation outcome. Annotation keywords include
title,description,default,examples,readOnly,writeOnly, anddeprecated. Validators may collect and report annotation values, but they do not use them to accept or reject data. - vocabulary
- A named set of JSON Schema keywords with defined semantics. The annotation keywords belong to the meta-data vocabulary in the JSON Schema specification. Other vocabularies include the validation vocabulary (
type,minimum,required, etc.) and the applicator vocabulary (allOf,anyOf,if/then/else). Vocabularies can be declared required or optional in a meta-schema. - default
- A JSON Schema annotation keyword whose value is the value to assume when the annotated property is absent from the instance. It must be valid against the property's schema. The
defaultkeyword is informational only unless the validator is configured to apply it — Ajv requiresuseDefaults: trueto write defaults into data objects during validation. - readOnly
- A boolean annotation keyword indicating that a property's value is managed by the server and should not be included in client write operations (POST, PUT, PATCH). Introduced in JSON Schema Draft-07. Not enforced by validators — enforcement is the API layer's responsibility. Used by OpenAPI tools to exclude the property from generated request body schemas and examples.
- deprecated
- A boolean annotation keyword (value
true) indicating that the annotated schema or property is obsolete and should no longer be used. Introduced in JSON Schema Draft 2019-09. Does not affect validation. Used by documentation tools (Swagger UI shows a strike-through) and SDK generators (emit@Deprecatedannotations or deprecation warnings in generated client code).
Frequently asked questions
Do annotation keywords like title and description affect JSON Schema validation?
No. Annotation keywords — title, description, default, examples, deprecated, readOnly, and writeOnly — are purely metadata. A validator such as Ajv collects and reports them but they have no effect on whether data passes or fails validation. A schema with title: "User ID" and one without it behave identically during validation. Annotations exist to communicate intent to humans and tools — documentation generators, form builders, API clients — rather than to enforce constraints.
Does Ajv apply default values automatically?
Not by default. The default keyword is an annotation — Ajv reads it but does not write the value into your data unless you enable the useDefaults option: new Ajv({ useDefaults: true }). With useDefaults: true, Ajv populates missing object properties from their default values during validation. It does not fill in missing array items. For nested objects, defaults are applied depth-first. Be aware that useDefaults mutates the input object in place, so pass a clone if you need to preserve the original.
What is the difference between examples (JSON Schema) and example (OpenAPI)?
JSON Schema Draft-07 introduced examples as an array of valid values (plural). OpenAPI 2.0 and 3.0 use example (singular, no array) for backward compatibility with Swagger. OpenAPI 3.1, which is fully aligned with JSON Schema Draft 2020-12, supports both: examples (array) from JSON Schema and the OpenAPI-specific example object map in the Media Type Object. If you write schemas for Ajv or a JSON Schema validator, use examples (array). If you write OpenAPI 3.0 specs, use example (singular). In OpenAPI 3.1 you can use either or both.
What does readOnly mean in JSON Schema?
readOnly: true is an annotation indicating that the property value is managed by the server and must not be sent in write requests (POST/PUT/PATCH). Common examples are id, createdAt, and updatedAt fields. JSON Schema validators do not enforce readOnly — a validator will not fail if a client sends a readOnly field. Enforcement is the responsibility of the API layer. OpenAPI uses readOnly to filter out those fields when generating request body examples and to mark them as read-only in generated SDK clients.
What does writeOnly mean in JSON Schema?
writeOnly: true marks a property that is accepted in requests but never returned in responses. Passwords and secrets are the canonical example: a user submits a password in a registration request, but the API never echoes it back in any response body. Validators do not enforce writeOnly at runtime — it is a documentation signal for API generators, SDK tools, and form builders to omit the field from response examples and display it as a password input in UI forms.
How do I mark a field as deprecated in JSON Schema?
Add deprecated: true to the property schema. This keyword was introduced in Draft 2019-09. It signals to API consumers, SDK generators, and documentation tools that the field is still present for backward compatibility but should no longer be used. Validators like Ajv do not reject deprecated fields — they still validate normally. To communicate the replacement, combine deprecated with a description explaining the migration path, for example: "description": "Deprecated. Use preferredName instead."
How are JSON Schema annotations used in OpenAPI 3.1?
OpenAPI 3.1 is the first version fully aligned with JSON Schema (Draft 2020-12), so all JSON Schema annotations work natively. title populates field labels in Swagger UI. description renders as help text next to each field. default shows pre-filled values in the Try it out form. examples (array) feeds the example dropdown. readOnly fields are omitted from the request body schema in generated docs. writeOnly fields are omitted from response examples. deprecated fields are visually struck through in Swagger UI. OpenAPI 3.1 also adds its own extensions: x-internal (hide from public docs), externalDocs, and the discriminator object.
How do I collect annotation values with Ajv?
Ajv does not collect annotation values by default. To read annotations after validation, enable verbose mode and use ajv.validate() with the schema reference. Alternatively, traverse the schema manually to extract annotation values before or after validation. For useDefaults, Ajv writes default values into the data object during validation when useDefaults: true is set. For other annotations like title or description, the typical approach is to walk the schema with a utility like json-schema-traverse or use a documentation generator like Redoc or Swagger UI which natively reads all annotation keywords.
Further reading and primary sources
- JSON Schema Annotations — Official reference for JSON Schema annotation keywords
- Ajv useDefaults — Ajv documentation for populating defaults during validation