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:

LayerWhat it does with constraints
Codegen (types)Maps type, enum, format (for int64/date/binary), and nullability to language types
Runtime validatorEnforces assertions such as pattern, ranges, required, minLength; optionally format
Docs and mocksReads 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.
  • minLength and maxLength are assertions on code units; set them, because they also bound databases and UI inputs.
  • pattern is 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-time means RFC 3339; date means YYYY-MM-DD; byte means base64; binary means an opaque stream used for file bodies. Do not put binary inside a JSON property.
  • email, uuid, and uri are 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: true
  • minimum/maximum are inclusive; exclusiveMinimum/exclusiveMaximum are strict. In OpenAPI 3.1 they are numbers (draft 2020-12), not booleans.
  • multipleOf expresses steps such as cents or 0.25 increments.
  • int32 fits a JavaScript safe integer; int64 does not always. IDs above 2^53 lose precision in JS, so many APIs serialize int64 identifiers as strings. State that explicitly; a generator that emits number for 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.
  • uniqueItems is an assertion generators and validators both understand.
  • additionalProperties: false makes the shape closed. Use it for strict request bodies; use it cautiously on responses, where an additive field should not break older validators.
  • required is 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: 500
  • enum becomes a language union and a hard validation set; leave room for growth or document that unknown values are an error.
  • const pins a discriminator or fixed version exactly.
  • default documents 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.0 nullable: true is gone.

What each side actually receives

KeywordGenerated typeRuntime validatorMock / AI data
type, enum, constYesYesPicks a member
format: int64/date-time/byte/binaryYes (specialized)PartialFormats accordingly
format: email/uuid/uriUsually plain stringOptionalProduces plausible values
pattern, ranges, lengthNo (types stay wide)YesBoundary values
requiredOptional vs required fieldsYesOmits or includes correctly
defaultSometimes surfacedNoFills when omitted
example/examplesNoNoPreferred 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 format as guaranteed server-side validation; it is an annotation.
  • Forgetting anchors in pattern, so 'abc' passes a numeric rule.
  • Emitting int64 ids as JS numbers and losing precision.
  • Using 3.0 nullable: true in a 3.1 document, or wrapping nullability in a fake oneOf.
  • Setting additionalProperties: false on 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

  1. Assertions (required, ranges, pattern, lengths) match what the server actually enforces.
  2. format is chosen from the well-supported set and never treated as a security boundary.
  3. Large integers are serialized safely; date fields use the correct RFC 3339 variant.
  4. Arrays and strings are bounded; request objects are explicitly closed where appropriate.
  5. 3.1 nullability uses the type array; enums and consts reflect real allowed values.
  6. 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.