Skip to content

Types and validation

These pages show what each stack gets from your contract: the types you code against and the validators that refuse a bad request before your handler runs. They all use the same schema, CreateTask, trimmed here.

# contract/api.yaml, under schemas
CreateTask:
type: object
required: [title]
properties:
title: { type: string, minLength: 1, maxLength: 200 }
effort: { type: integer, minimum: 0 }

Each schema becomes one type and one validator per direction: input (what a client sends) and output (what your handler returns). Each operation also gets one input validator over its path, query, headers and body. An input validator refuses a key the contract does not declare.

Stack Types Validator You pick the validator?
NestJS TypeScript types zod (default), yup or class-validator Yes, with the validators entry
Express, Hono TypeScript types zod or yup Yes, with the validators entry
Fastify TypeScript types zod or yup, or Ajv when you link none Yes
AdonisJS TypeScript types zod or yup, or VineJS when you link none Yes
Spring Boot Java records A generated structural validator No
Laravel PHP classes Laravel rules rules only
Symfony PHP classes Symfony constraints rules only
FastAPI, Django Ninja Pydantic models Pydantic rules only
Django REST Framework dataclasses DRF serializers rules only

Server guides: NestJS, Express, Fastify, AdonisJS, Hono, Spring Boot, Laravel, Symfony, FastAPI, Django REST Framework, Django Ninja.

Client guides use the TypeScript types through the fetch client. The generators that write zod, yup and class-validator validators serve the server only.

To change the library on a Node server, see Switch the validator.

By default (rules: native) the generated validator uses the library’s own rules, such as .min(), uuid or Field(max_length=...). The library behaves as you know it, and in a few places it differs from the contract: how it counts the length of an emoji, or what it accepts as a date. Each library page lists them.

Set rules: seampact-consistent to get the contract’s exact checks instead. Every client, server and language then gives the same answer.

# seampact.config.yaml, under targets
- id: validators
use: "@seampact/codegen-yup"
apis: [notes]
options:
rules: seampact-consistent

The option exists on the zod, yup, class-validator, AdonisJS, Laravel, Symfony, FastAPI, Django REST Framework and Django Ninja targets. Spring Boot has no such option: its Java page says what is checked. The error body is the same in both modes.

A request the validator refuses never reaches your handler. The server answers 400 with the error api.schema_invalid, as application/problem+json.

{
"type": "api.schema_invalid",
"status": 400,
"code": "api.schema_invalid",
"nature": "refused",
"issues": [
{ "in": "body", "pointer": "/title", "rule": "minLength" },
{ "in": "body", "pointer": "/effort", "rule": "minimum" }
]
}

Each issue has three members.

  • in: where the value sits: path, query, header or body.
  • pointer: the value inside that part, as a JSON pointer. A pointer of "" is the whole body.
  • rule: the contract rule that failed, such as required, type, minLength, maxLength, minimum, format, enum or unknownKeys. It is never the library’s own error code, so a client reads the same rule from every server.

The reply lists every issue, up to 100. An unknown key gives one issue per key. The message text a library writes is its own and never travels in the body.

A contract can rename the code or reshape issues with a derived binding (Errors and natures). The members above are the defaults.