Define Schema
Reference: Typebox GitHub Repository
A contract is a function that takes one input and returns a cleaned value. Define builds one from two parts, a transform that shapes the output and optional guards that reject input before the transform runs.
The Shape Of Define
Define(transform, guard?) returns a contract:
import { Define } from '@neabyte/deserve'
// Transform only, no guard
const Trim = Define((body: { name: string }) => ({
name: body.name.trim()
}))The transform normalizes the value, trimming strings, lowercasing an email, or coercing a number. It runs as the contract body once the input is trusted.
The transform also owns the output shape. A guard that passes does not strip extra keys, so unknown fields from the client survive unless the transform leaves them out. Returning a fresh object with only the wanted fields keeps surprise input out of the validated data:
import { Define } from '@neabyte/deserve'
// Output holds only the named fields
const NewUser = Define((body: { name: string; role: string }) => ({
name: body.name.trim()
}))Here a client sending role: 'admin' finds it dropped, since the transform never copies it forward.
Order Of Operations
Calling a contract runs four steps in a fixed order, and the transform only ever sees input that cleared every guard:
- A string input longer than 10000 characters is rejected before anything else.
- An object input is deep frozen so a guard cannot mutate it.
- Each guard runs in order, throwing on the first failure.
- The transform runs and returns the cleaned value.
A contract with no guard skips straight to the transform, so the transform must trust its input or do its own checks.

Guards Decide Pass Or Fail
A guard inspects the raw input and returns a verdict:
truewhen the input passes.- A
stringfor a single failure reason. - A
string[]for several failure reasons at once.

import { Define } from '@neabyte/deserve'
// Guard rejects an empty name
const NewUser = Define(
(body: { name: string }) => ({ name: body.name.trim() }),
(body) => (body.name.trim().length > 0 ? true : 'name must not be empty')
)A guard that returns reasons makes the contract throw, and the validator turns that throw into a 422 carrying those exact reasons. The path from a reason to a response lives in Reading Validated Data.
Guarding The Shape First
A guard receives the raw input, which can be null, an array, or any JSON value a client sends. Reaching for a field on the wrong shape throws inside the guard before the rule even runs, so a shape check comes first:
import { Define } from '@neabyte/deserve'
// Confirm an object before reading fields
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
const NewUser = Define(
(body: { name: string }) => ({ name: body.name.trim() }),
(body) => {
if (!isRecord(body)) {
return 'body must be a JSON object'
}
return typeof body['name'] === 'string' ? true : 'name must be a string'
}
)A throw inside a guard still becomes a 422, never a 500, so a missed shape check fails safe rather than crashing the request.
Reporting Several Fields At Once
Returning an array reports every broken field in a single response instead of one at a time:
import { Define } from '@neabyte/deserve'
// Collect each failure into one array
const NewUser = Define(
(body: { name: string; age: number }) => body,
(body) => {
const reasons: string[] = []
if (body.name.trim().length === 0) {
reasons.push('name must not be empty')
}
if (body.age < 18) {
reasons.push('age must be at least 18')
}
return reasons.length === 0 ? true : reasons
}
)Composing Several Guards
The second argument also takes an array of guards. They run in order and the contract throws on the first one that fails, so later guards never see input that an earlier guard already rejected:
import { Define } from '@neabyte/deserve'
// Shape check first, business rule second
function hasFields(body: { from: string; to: string }): true | string {
return body.from && body.to ? true : 'from and to are required'
}
function distinctAccounts(body: { from: string; to: string }): true | string {
return body.from !== body.to ? true : 'from and to must differ'
}
const Transfer = Define(
(body: { from: string; to: string }) => body,
[hasFields, distinctAccounts]
)Splitting a shape check from a business rule keeps each guard small and lets the cross-field rule assume the fields already exist.
Built-In Safety
The string cap and the freeze from Order Of Operations run automatically, so a contract never burns time on a huge payload and a guard never mutates the value it inspects. One more rule guards the timing model:
- An async guard is rejected, since validation stays synchronous and predictable.
These rules come from Typebox itself and apply to every contract, whether it runs through the Validator Middleware or a direct Validator.check call.
Where To Go Next
- Validator Middleware - wire contracts to request sources.
- Reading Validated Data - read the transform output in a handler.
- Advanced Patterns - compose guards and order their failures.
- Validation Overview - how the pieces fit together.