JSON Schema default Keyword: Ajv useDefaults, OpenAPI, and Forms

Last updated:

The JSON Schema default keyword annotates a property with its default value — validators like Ajv can optionally apply it to missing properties when useDefaults: true is set. default is an annotation, not a validation constraint; a value that differs from defaultstill validates. Ajv's useDefaults: true mutates the validated object in-place — useful for config normalization but a side-effect to be aware of.

This guide covers the defaultkeyword spec, Ajv's useDefaults option, OpenAPI default semantics, JSON Schema form libraries, and common pitfalls like shared mutable defaults. You will leave with a clear mental model of when and how to apply defaults — and where they silently bite you.

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 Jsonic

What the JSON Schema default keyword actually does

The default keyword is defined in the JSON Schema meta-data vocabulary — the same vocabulary as title, description, examples, deprecated, readOnly, and writeOnly. All of these keywords are annotations: they attach information to a schema without influencing the pass/fail outcome of validation. See the JSON Schema annotations guide for the full vocabulary.

A validator that encounters default must not use it to reject data. If a property declares default: 20 and the instance provides 99, the validator evaluates 99 against type, minimum, maximum, and so on — ignoring default entirely. If the property is absent from the instance, the validator does not write 20 into the data — it simply skips the property.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "pageSize": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 20,
      "description": "Number of results per page. Assumed to be 20 when omitted."
    },
    "sortOrder": {
      "type": "string",
      "enum": ["asc", "desc"],
      "default": "asc"
    }
  }
}

// Both of these validate successfully — default is not a constraint:
// { "pageSize": 50, "sortOrder": "desc" }   ✓ normal case
// {}                                          ✓ empty object also valid
// { "pageSize": 1 }                           ✓ sortOrder absent — no error

The value of defaultshould itself be valid against the property's schema — putting a string in a default on an integer property is a schema authoring error, not a validation error. Tools that read default for documentation or UI rendering will display a confusing value if it does not match the declared type.

Because default is an annotation, Ajv and other validators may collect and expose it in their output (along with title, description, andexamples) but they do not act on it unless explicitly configured.

Ajv useDefaults: populate missing fields automatically

Ajv is the most widely used JSON Schema validator for JavaScript and TypeScript. Out of the box it treats default as a read-only annotation. To make Ajv write defaults into your data during validation, pass { useDefaults: true } to the constructor. See the Ajv JSON Schema validator guide for a broader introduction to Ajv configuration.

import Ajv from 'ajv'

const ajv = new Ajv({ useDefaults: true })

const schema = {
  type: 'object',
  properties: {
    pageSize:  { type: 'integer', default: 20 },
    sortOrder: { type: 'string',  default: 'asc' },
    role:      { type: 'string',  default: 'viewer' },
  },
}

const validate = ajv.compile(schema)

const data = { sortOrder: 'desc' }   // pageSize and role are absent

validate(data)

console.log(data)
// { sortOrder: 'desc', pageSize: 20, role: 'viewer' }
//
// Ajv mutated the object in place — the two missing fields now have their defaults

The mutation happens before Ajv runs validation checks, which means the injected defaults are also validated against the schema. If a default value fails the schema's own constraints (for example, a default string that is too short), Ajv will report a validation error.

There is also a useDefaults: "empty" variant: instead of filling only absent properties, it also replaces properties whose value is undefined (distinct from null). This is useful in environments where JSON deserialization produces undefined for missing keys rather than omitting them.

Defaults in nested objects and arrays with Ajv

Ajv applies defaults depth-first. If a nested object property is present in the data, Ajv descends into it and fills defaults for its missing sub-properties. If the nested object itself is absent, Ajv does not create it — unless you declare a default on the parent property as well.

import Ajv from 'ajv'

const ajv = new Ajv({ useDefaults: true })

const schema = {
  type: 'object',
  properties: {
    // Parent has default: {} so Ajv creates it when absent
    notifications: {
      type: 'object',
      default: {},
      properties: {
        email: { type: 'boolean', default: true },
        push:  { type: 'boolean', default: false },
        sms:   { type: 'boolean', default: false },
      },
    },
    theme: { type: 'string', default: 'light' },
  },
}

const validate = ajv.compile(schema)

// Case 1: notifications is absent entirely
const data1 = {}
validate(data1)
console.log(data1)
// { notifications: { email: true, push: false, sms: false }, theme: 'light' }
// Step 1: Ajv writes notifications: {} (parent default)
// Step 2: Ajv descends and fills email, push, sms defaults

// Case 2: notifications present but partially filled
const data2 = { notifications: { email: false } }
validate(data2)
console.log(data2)
// { notifications: { email: false, push: false, sms: false }, theme: 'light' }
// email kept as false (present); push and sms filled from defaults

For arrays, Ajv does not fill in defaults for missing array items. The useDefaults option only applies to object properties. If you need default items in an array, you must pre-populate the array in application code before passing it to the validator, or use a custom Ajv keyword.

Default in OpenAPI 3.x: parameters, requestBody, response schemas

OpenAPI 3.x uses default in two distinct contexts that are easy to confuse. Understanding both is essential when writing API specifications. See the JSON Schema and OpenAPI guide for deeper coverage.

1. default on a Parameter Object — This is a behavioral API contract. When a client omits a query, header, or cookie parameter, the server must behave as if the client had sent the declared default value. This is not just documentation: it is a semantic commitment that the server will apply the default server-side.

# OpenAPI 3.1 — default on a Parameter Object (behavioral contract)
paths:
  /products:
    get:
      parameters:
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20   # Server uses 20 when client omits ?pageSize
        - name: sortOrder
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc  # Server sorts ascending when client omits ?sortOrder

2. default on a Schema Object (inside requestBody or responses) — This is a JSON Schema annotation, the same as in a standalone JSON Schema document. It informs documentation tools and client generators about the assumed value when the property is absent, but imposes no server-side behavior requirement.

# OpenAPI 3.1 — default inside a Schema Object (annotation only)
components:
  schemas:
    CreateProductRequest:
      type: object
      properties:
        name:
          type: string
        currency:
          type: string
          default: USD        # Documentation annotation — no server obligation
        taxIncluded:
          type: boolean
          default: false      # Swagger UI pre-fills false in Try-it-out form
      required:
        - name

Swagger UI and Redoc use schema-level default to pre-fill the Try-it-out form fields. OpenAPI 3.1 aligns Schema Objects with JSON Schema Draft 2020-12, so schema-level default is a pure annotation in 3.1. In OpenAPI 3.0, the behavior is the same in practice but the specification was less explicit about the annotation vs. constraint distinction.

JSON Schema form libraries and default values

Form libraries that generate UI from JSON Schema read default to pre-fill fields. This is one of the most practical use cases for the keyword outside of validators. See the JSON Schema for forms guide for a full walkthrough.

react-jsonschema-form (rjsf) is the most widely used JSON Schema form library for React. It reads default and populates the form field with that value when no formData value is provided. Nested object defaults are applied recursively.

import Form from '@rjsf/core'
import validator from '@rjsf/validator-ajv8'

const schema = {
  type: 'object',
  title: 'User Preferences',
  properties: {
    theme: {
      type: 'string',
      enum: ['light', 'dark', 'system'],
      default: 'system',        // rjsf pre-selects 'system' in the dropdown
      title: 'Color Theme',
    },
    language: {
      type: 'string',
      default: 'en',
      title: 'Language',
    },
    emailDigest: {
      type: 'string',
      enum: ['daily', 'weekly', 'never'],
      default: 'weekly',
      title: 'Email Digest Frequency',
    },
  },
}

export default function PreferencesForm() {
  return (
    <Form
      schema={schema}
      validator={validator}
      onSubmit={({ formData }) => savePreferences(formData)}
    />
  )
  // rjsf renders a form with 'system', 'en', and 'weekly' pre-filled
}

json-forms (from EclipseSource) and Uniforms follow the same pattern: the default value is applied as the initial field value. Both support nested object defaults through their schema traversal logic.

A critical distinction: rjsf applies defaults from the schema only when formData is undefined or does not contain the field. If you pass partial formData, rjsf merges the defaults for the missing fields. This mirrors Ajv's useDefaults behavior and means you can use the same schema for both form rendering and server-side validation.

Common pitfalls: mutable defaults, $ref + default, and required vs default

The default keyword has several footguns that cause subtle, hard-to-debug bugs. Understanding them upfront saves hours of debugging.

Pitfall 1: Shared mutable defaults

When Ajv compiles a schema, it stores the default values from the schema object in memory. With useDefaults: true, Ajv writes a reference to that stored default into each validated data object. For primitives (strings, numbers, booleans), this is safe. For arrays and objects, every validated instance receives a reference to the same object. Mutating one instance mutates all future instances:

import Ajv from 'ajv'

const ajv = new Ajv({ useDefaults: true })

const schema = {
  type: 'object',
  properties: {
    // DANGER: array default is a shared reference
    tags: { type: 'array', items: { type: 'string' }, default: [] },
  },
}

const validate = ajv.compile(schema)

const a = {}
const b = {}

validate(a)
validate(b)

a.tags.push('urgent')   // mutates the shared default array!

console.log(b.tags)     // ['urgent'] — b.tags is the same array object as a.tags

// FIX: deep-clone after validation, or use a factory function
import structuredClone from '@ungap/structured-clone'
const safeA = structuredClone({})
validate(safeA)
safeA.tags.push('urgent')
console.log(b.tags)     // [] — b is unaffected

Pitfall 2: $ref and default

In JSON Schema, a $ref replaces the schema at that location — sibling keywords next to a $ref are ignored in Draft-07 and earlier. This means placing default next to a $ref has no effect:

// Draft-07: default is ignored — $ref replaces the schema
{
  "$ref": "#/$defs/Role",
  "default": "viewer"   // silently ignored in Draft-07
}

// Draft 2019-09+: $ref is a keyword, siblings are merged — default works
{
  "$ref": "#/$defs/Role",
  "default": "viewer"   // applied in Draft 2019-09+ and JSON Schema 2020-12
}

// Safe approach in any draft: put default inside the $def
{
  "$defs": {
    "Role": {
      "type": "string",
      "enum": ["admin", "editor", "viewer"],
      "default": "viewer"   // always works regardless of draft
    }
  }
}

Pitfall 3: required vs default

Declaring a field as both required and giving it a defaultis contradictory outside of Ajv's useDefaults mode. Without useDefaults, if the field is absent, validation fails on required before the default annotation is ever consulted. The default never gets applied. The correct pattern: use default without required for fields the system can fill in; use required without default for fields the client must always provide explicitly.

Design patterns: config normalization, partial objects, and TypeScript defaults

The most practical use of default with Ajv's useDefaults is config normalization — accepting a partial configuration object and filling in all missing values with sensible defaults before the application uses it. See the JSON Schema guide for foundational concepts.

import Ajv from 'ajv'
import type { JSONSchemaType } from 'ajv'

// Full config type — all fields are required after normalization
interface AppConfig {
  port: number
  host: string
  maxConnections: number
  timeout: number
  logging: {
    level: 'debug' | 'info' | 'warn' | 'error'
    format: 'json' | 'text'
  }
}

const schema: JSONSchemaType<AppConfig> = {
  type: 'object',
  properties: {
    port:           { type: 'integer', default: 3000 },
    host:           { type: 'string',  default: '0.0.0.0' },
    maxConnections: { type: 'integer', default: 100 },
    timeout:        { type: 'integer', default: 30000 },
    logging: {
      type: 'object',
      default: {},
      properties: {
        level:  { type: 'string', enum: ['debug', 'info', 'warn', 'error'], default: 'info' },
        format: { type: 'string', enum: ['json', 'text'], default: 'json' },
      },
      required: ['level', 'format'],
    },
  },
  required: ['port', 'host', 'maxConnections', 'timeout', 'logging'],
}

const ajv = new Ajv({ useDefaults: true })
const validate = ajv.compile(schema)

// Accept partial config from user — only override what they specify
function normalizeConfig(partial: Partial<AppConfig>): AppConfig {
  // Deep-clone to avoid mutable default contamination
  const config = JSON.parse(JSON.stringify(partial))
  if (!validate(config)) {
    throw new Error('Invalid config: ' + ajv.errorsText(validate.errors))
  }
  return config as AppConfig  // useDefaults guarantees all required fields present
}

const config = normalizeConfig({ port: 8080 })
// { port: 8080, host: '0.0.0.0', maxConnections: 100, timeout: 30000,
//   logging: { level: 'info', format: 'json' } }

The JSON.parse(JSON.stringify(partial)) deep-clone on line before validation prevents the shared mutable default problem. It also converts undefined values to omitted keys, which useDefaults then fills.

For TypeScript, the key insight is that after validate(config) returns true with useDefaults: true, all required fields are guaranteed to be present — so the as AppConfig assertion is safe. If you want to avoid the assertion, wrap the function to return AppConfig | null and check the Ajv result.

Definitions

Annotation keyword
A JSON Schema keyword that attaches metadata to a schema or property without affecting validation outcome. default, title, description, examples, deprecated, readOnly, and writeOnly are all annotation keywords. Validators may collect and expose annotation values but never use them to accept or reject data.
Validation constraint
A JSON Schema keyword that determines whether data passes or fails validation. Examples include type, minimum, maximum, required, minLength, pattern, and enum. Constraint keywords are in the validation vocabulary, distinct from the meta-data (annotation) vocabulary.
useDefaults
An Ajv constructor option ({ useDefaults: true }) that makes Ajv populate missing object properties with their default values during validation. Mutation happens in-place on the input object before validation checks run. The variant useDefaults: "empty" also fills properties whose value is undefined.
Mutable default
A defaultvalue that is an array or object — types whose contents can be changed after creation. When Ajv writes a mutable default into validated data, it writes a reference to the compiled schema's stored default object, not a copy. All instances that receive that default share the same reference. Mutating one instance mutates all others. Fix by deep-cloning the input before validation.
Schema composition
Combining multiple schemas using allOf, anyOf, oneOf, or $ref. The interaction of default with $ref is draft-dependent: in Draft-07, a $ref replaces the entire schema at that location, silently discarding sibling keywords like default. In Draft 2019-09+, $ref is a keyword and siblings are merged, so default next to $ref is honored.
Required vs optional
In JSON Schema, required is an array on the parent object schema listing property names that must be present in the instance. A property not inrequired is optional — it may be absent. Combining required with default on the same property is valid JSON Schema but contradictory in practice: without useDefaults, missing a required field fails validation before the default annotation is consulted.
Deep clone
Creating a structurally identical copy of an object or array where all nested objects and arrays are also new copies — no shared references. In JavaScript, JSON.parse(JSON.stringify(obj)) is the most portable deep clone for JSON-serializable data. structuredClone(obj) (available in Node.js 17+ and modern browsers) handles more types including Date and Map. Deep-cloning before passing data to Ajv with useDefaults: true prevents shared mutable default contamination.

Frequently asked questions

What does the default keyword do in JSON Schema?

The default keyword in JSON Schema is an annotation that declares the value a property is assumed to have when it is absent from the instance. It does not enforce anything — a validator like Ajv reads the default value as metadata but will not fail data that omits the property, nor will it automatically write the default into the validated object. Think of it as a documented hint to humans, form builders, documentation generators, and API tools about what value makes sense when the field is missing. The actual application of defaults is opt-in and tool-specific: Ajv requires useDefaults: true, OpenAPI clients use it for pre-filled forms, and form libraries like react-jsonschema-form render it as the pre-filled field value.

Does JSON Schema validate that a value matches the default?

No. JSON Schema does not validate that a provided value matches the declared default. The default keyword is a pure annotation — it carries no validation semantics. If a property has default: 20 and the data provides 99, the validator does not fail. The value 99 is evaluated only against the actual validation keywords on that property (type, minimum, maximum, enum, etc.) — ignoring default entirely. This behavior is intentional: defaults communicate intent, not constraints. If you want to constrain the value, use enum, minimum/maximum, or const — not default.

How do I make Ajv automatically fill in default values?

Pass useDefaults: true to the Ajv constructor: const ajv = new Ajv({ useDefaults: true }). With this option enabled, Ajv mutates the validated data object in place — adding any missing object properties using their declared default values before returning the validation result. For example, if the schema declares { "role": { "type": "string", "default": "viewer" } } and the data omits role, Ajv writes role: "viewer" into the data object. Important caveats: useDefaults only applies to missing object properties, not missing array items. It mutates the original input, so clone your data first if you need to preserve the unmodified original. If a property is present but explicitly set to null, Ajv does not replace it with the default.

Can I use default with required fields in JSON Schema?

You can declare a property in both required and with a default, but the combination is logically contradictory and practically confusing. If a field is required, validation fails when it is absent — the default is never applied because the schema already rejects the data. The only scenario where default on a required field makes sense is when you use Ajv with useDefaults: true: Ajv applies the default before validating, so the required check passes even if the property was originally missing. Outside of Ajv's useDefaults, combining required and default serves no purpose for validation. The idiomatic approach is: use required for fields that the client must always provide explicitly, and use default (without required) for fields that the system can fill in when omitted.

What is the difference between default in JSON Schema and default in OpenAPI?

In JSON Schema, default is a metadata annotation on a schema property — it says "assume this value when the property is absent." It has no effect on validation. In OpenAPI 3.x, default has an additional semantic on parameter objects: when a client omits a parameter (query, header, path, cookie), the server is expected to behave as if the client had sent the default value. This is an API contract, not just a documentation hint. For Schema Objects inside OpenAPI (describing request/response bodies), default behaves the same as in JSON Schema — it is an annotation. The key distinction: default on an OpenAPI Parameter Object is a server-side behavioral commitment; default inside a Schema Object is metadata. OpenAPI 3.1 aligns Schema Objects with JSON Schema Draft 2020-12, so schema-level defaults are pure annotations.

How do nested objects get default values with Ajv useDefaults?

Ajv applies useDefaults depth-first through nested object schemas. If the parent object is present in the data, Ajv descends into it and fills defaults on its properties. If the parent object itself is missing from the data, Ajv does not create it automatically — only the immediate missing properties of objects that exist in the data are populated. To get defaults for an entirely missing nested object, you must declare a default for the parent property: { "notifications": { "type": "object", "default": {}, "properties": { "email": { "type": "boolean", "default": true } } } }. With this schema and useDefaults: true, if notifications is absent, Ajv first writes notifications: {} (from the parent default), then descends and writes email: true inside it.

What happens when two schemas share the same default array object?

When a schema is compiled with Ajv and useDefaults: true, Ajv reuses the same default value object across all validations using that compiled schema. For primitive defaults (strings, numbers, booleans), this is safe — they are immutable. For mutable defaults (arrays and objects), Ajv writes a reference to the same object into every validated data instance. If one instance mutates the default array by pushing a value, all subsequent validations share that mutated state. This is a well-known bug pattern. The fix is to deep-clone the input before passing it to Ajv: const data = JSON.parse(JSON.stringify(partial)) and then validate(data). This creates a fresh object each time, so Ajv writes the default reference into a throwaway copy rather than a shared reference.

How do I use JSON Schema defaults with TypeScript types?

The most common pattern is to use a type generator like json-schema-to-typescript to produce TypeScript interfaces from your JSON Schema, then use Ajv with useDefaults: true at runtime to fill in defaults. After validation, TypeScript knows all required fields are present. For optional fields with defaults, you can use Partial<T> before validation and the full T type after. A cleaner approach is to split the schema: define an "input" schema where defaulted fields are optional, and validate with useDefaults: true to produce data that satisfies the "full" schema. TypeScript's type system does not understand that a runtime validator has filled in defaults, so you may need a type assertion (data as FullConfig) or a wrapper function that returns the narrowed type after validation.

Further reading and primary sources