The JSON Schema constraints that actually work in OpenAPI: format, pattern, ranges, and what codegen and AI mocks do with them
Constraints are where an OpenAPI document either earns its keep or starts lying. Add format: uuid and one developer assumes the server validates it, another assumes the SDK parses it, and a generated mock happily emits "not-a-uuid". The confusion comes from treating every JSON Schema keyword as the same kind of rule. They are not. Some produce types, some produce runtime validation, and some are annotations that nothing enforces unless you wire it up.
Annotation versus assertion
In JSON Schema, format is an annotation by default. A validator may check it, but it is not required to, and unknown formats must be ignored rather than fail validation. Keywords like minimum, pattern, and required are assertions: a compliant validator enforces them. OpenAPI generators and validators split the work three ways:
| Layer | What it does with constraints |
|---|---|
| Codegen (types) | Maps type, enum, format (for int64/date/binary), and nullability to language types |
| Runtime validator | Enforces assertions such as pattern, ranges, required, minLength; optionally format |
| Docs and mocks | Reads everything, including annotations and example, to render and synthesize data |
Do not rely on format for security validation. If a malformed email must be rejected, enforce it server-side and document the 422; treat format as a strong hint to clients and tooling.
Strings that do real work
EmailAddress:
type: string
format: email
maxLength: 254
OrderReference:
type: string
pattern: '^ORD-[0-9]{8}$'
example: ORD-20261007
ExternalUrl:
type: string
format: uri
maxLength: 2048
CreatedAt:
type: string
format: date-time
description: RFC 3339 timestamp in UTC.minLengthandmaxLengthare assertions on code units; set them, because they also bound databases and UI inputs.patternis a full regex assertion. Anchor it (^...$) when you mean the whole value, otherwise it matches a substring. Keep it simple enough that a client can reproduce it.format: date-timemeans RFC 3339;datemeansYYYY-MM-DD;bytemeans base64;binarymeans an opaque stream used for file bodies. Do not putbinaryinside a JSON property.email,uuid, anduriare the formats with the broadest tooling support. Anything custom is effectively documentation.
Numbers, and the int64 trap
Quantity:
type: integer
minimum: 1
maximum: 9999
example: 12
DiscountRate:
type: number
exclusiveMinimum: 0
maximum: 1
multipleOf: 0.01
LedgerId:
type: integer
format: int64
description: Serializes as a string to preserve precision in JavaScript clients.
x-clients-string: trueminimum/maximumare inclusive;exclusiveMinimum/exclusiveMaximumare strict. In OpenAPI 3.1 they are numbers (draft 2020-12), not booleans.multipleOfexpresses steps such as cents or 0.25 increments.int32fits a JavaScript safe integer;int64does not always. IDs above 2^53 lose precision in JS, so many APIs serializeint64identifiers as strings. State that explicitly; a generator that emitsnumberfor a ledger id is a latent bug.
Arrays and objects
Tags:
type: array
items: { type: string, maxLength: 24 }
minItems: 1
maxItems: 10
uniqueItems: true
Address:
type: object
required: [country, locality]
additionalProperties: false
properties:
country: { type: string, pattern: '^[A-Z]{2}$' }
locality: { type: string, maxLength: 120 }
postalCode: { type: string, maxLength: 16 }- Bound arrays on both ends when you can; unbounded arrays hide pagination mistakes.
uniqueItemsis an assertion generators and validators both understand.additionalProperties: falsemakes the shape closed. Use it for strict request bodies; use it cautiously on responses, where an additive field should not break older validators.requiredis the one assertion teams most often omit, which turns every field optional in the generated type.
Enums, const, default, and nullability
InvoiceStatus:
type: string
enum: [draft, open, paid, void]
WebhookVersion:
type: string
const: '2026-10-01'
Currency:
type: string
enum: [USD, EUR, GBP]
default: USD
OptionalNote:
type: [string, 'null']
maxLength: 500enumbecomes a language union and a hard validation set; leave room for growth or document that unknown values are an error.constpins a discriminator or fixed version exactly.defaultdocuments what the server assumes when the field is omitted; it does not force the client to send it.- In 3.1, nullable is
type: [string, 'null']. The old 3.0nullable: trueis gone.
What each side actually receives
| Keyword | Generated type | Runtime validator | Mock / AI data |
|---|---|---|---|
type, enum, const | Yes | Yes | Picks a member |
format: int64/date-time/byte/binary | Yes (specialized) | Partial | Formats accordingly |
format: email/uuid/uri | Usually plain string | Optional | Produces plausible values |
pattern, ranges, length | No (types stay wide) | Yes | Boundary values |
required | Optional vs required fields | Yes | Omits or includes correctly |
default | Sometimes surfaced | No | Fills when omitted |
example/examples | No | No | Preferred sample |
The gap in the first data row is deliberate: a TypeScript string cannot encode maxLength: 254. Types stay wide on purpose; assertions are enforced at the boundary, not in the type system.
How mocks and AI use the same fields
A spec-driven mock does not invent values at random; it walks the constraints. Given pattern: '^ORD-[0-9]{8}$' it returns a matching reference; given minimum: 1, maximum: 9999 it can generate both a normal value and the boundaries 1 and 9999; given an enum it cycles variants so you see every UI state. AI agents generating request bodies do the same when the constraints are present, and they hallucinate far less: a UUID field gets a UUID, a closed object gets no extra keys, a tagged union gets the right discriminator. Sparse schemas produce sparse, confident-sounding fiction. Tight schemas produce requests that pass on the first try.
This is also how to test edge cases deliberately: ask the mock for minimum, maximum, empty, and null variants of each field, then confirm the client and server agree. The contract already encodes those cases.
Common mistakes
- Treating
formatas guaranteed server-side validation; it is an annotation. - Forgetting anchors in
pattern, so'abc'passes a numeric rule. - Emitting
int64ids as JS numbers and losing precision. - Using 3.0
nullable: truein a 3.1 document, or wrapping nullability in a fakeoneOf. - Setting
additionalProperties: falseon responses and breaking clients on additive, backward-compatible fields. - Omitting
required, which silently makes the generated type all-optional. - Writing constraints the server does not actually enforce, so the contract promises rejections that never happen. Validate against the real framework rules, not the spec's aspirations.
Checklist
- Assertions (
required, ranges,pattern, lengths) match what the server actually enforces. formatis chosen from the well-supported set and never treated as a security boundary.- Large integers are serialized safely; date fields use the correct RFC 3339 variant.
- Arrays and strings are bounded; request objects are explicitly closed where appropriate.
- 3.1 nullability uses the type array; enums and consts reflect real allowed values.
- Mocks generate valid, boundary, empty, and null cases from the same constraints.
When types, validators, and mocks all read the same tight schema, "it worked in the docs" finally means "it works against the server."
Tighten a schema, generate the client, and watch a mock produce boundary-correct data from the same constraints, in the browser app. Constraints and examples work as a pair; the pattern for keeping them in sync across docs, mocks, and agents is in the OpenAPI examples as a single source of truth guide.