# 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

``` yaml
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

``` yaml
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

``` yaml
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

``` yaml
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

| 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 `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](https://www.powerduck.com/app/?ref=powerduck.com). 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](https://www.powerduck.com/blog/openapi-examples-single-source-of-truth/).

