The schema language is a compact DSL for API contracts: you declare the shape traffic is supposed to have, and SchemaClient validates real traffic against it — live in the app across REST, GraphQL, WebSocket, SSE and gRPC, and in CI via the CLI. One source file can describe every protocol your API speaks. This page is the complete syntax reference; for the workflow, start with schema validation.
Comments, type aliases and schema blocks
Comments start with # and run to the end of the line — chosen so they never collide
with // inside URL strings. There are two top-level declarations:
# A reusable alias
type Email = string & format("email")
# A named contract — body is an object block or `= Type`
schema User {
id: string & format("uuid")
email: Email
role: "admin" | "user" | "guest"
}
Commas and semicolons between fields are optional — newlines are enough. A schema can
extend another object schema; redeclaring an inherited field requires the override
prefix, otherwise you get a diagnostic. Type aliases take generic parameters:
type Page<T> = { items: T[], total: integer }
schema Response(200) = Page<User>
Primitives and constraint chains
Eight primitives: string, number, integer, boolean, object, any,
unknown, null. integer rejects non-whole numbers; any and unknown match
everything; object matches any non-array object.
Constraints chain onto a type with &:
type Username = string & minLength(3) & maxLength(32) & pattern("[a-z0-9_]+")
type Age = integer & min(0) & max(150)
type Tags = string[] & minItems(1) & maxItems(20) & uniqueItems
| Applies to | Constraints |
| --- | --- |
| string | minLength(n), maxLength(n), pattern("regex"), format("name") |
| number / integer | min(n), max(n) (aliases: minimum, maximum), multipleOf(n) |
| arrays | minItems(n), maxItems(n), uniqueItems |
| any type | enum(a, b, c) |
| documentation only | description, example, default, deprecated — no runtime effect |
pattern is anchored: the regex must match the entire string, not a substring. An
unknown constraint name is a warning, not an error, and a duplicated constraint is
ignored with a warning.
Named formats
format("name") validates a string against one of sixteen built-in formats:
| Format | Example |
| --- | --- |
| email | user@example.com |
| uuid | 550e8400-e29b-41d4-a716-446655440000 |
| date | 2026-01-15 |
| time | 14:30:00Z |
| date-time | 2026-01-15T14:30:00Z |
| ipv4 | 192.168.1.1 |
| ipv6 | 2001:db8::1 |
| hostname | api.example.com |
| uri / url | https://example.com/path |
| json-pointer | /users/0/name |
| e164 | +14155552671 |
| slug | hello-world |
| alphanumeric | abc123 |
| hex-color | #ff8800 |
| credit-card | 4111111111111111 (Luhn check) |
An unknown format name downgrades to a warning so a typo never silently passes as a hard failure in the wrong direction.
Fields and type expressions
schema Example {
id: string # required
nickname?: string # optional — may be absent
bio: string? # nullable — may be null
avatar?: string? # both
tags: string[] # array
role: "admin" | "user" # union of literals
point: [number, number] # tuple — fixed length and order
address: { city: string, zip?: string } # nested object, any depth
"x-trace": string # quote names that are not identifiers
}
Fields are required by default. ? after the name makes the field optional; ?
after the type makes the value nullable — these are independent, and the validator
reports missing-required and forbidden-null as distinct errors. Literals can be
strings, numbers or booleans. Unions must be unambiguous: a value that matches
more than one variant is an error, not a pass, which keeps discriminated unions
honest.
Protocol decorators
A decorator binds a schema to live traffic. The role names (Request, Response,
Params, Send, …) are scoped to their decorator address, so every endpoint
declares its own set without collisions.
@http
@http("POST /users")
schema Request { email: Email, name: string & minLength(1) }
@http("POST /users")
schema Response(201) { id: string & format("uuid") }
@http("POST /users")
schema Response(400) { error: string, field: string? }
@http("GET /users/{id}")
schema Params { id: string & format("uuid") }
Roles: Request, Response(status), Params, Headers, Cookies. A bare
Response defaults to status 200. @endpoint is accepted as an alias for @http.
@graphql
@graphql("query GetUser")
schema Variables { id: string & format("uuid") }
@graphql("query GetUser")
schema Data { user: User }
Roles: Variables, Data, Errors. Schemas are matched by the operation name in a
query, mutation or subscription — see GraphQL requests.
@websocket
@websocket("/ws/chat", "type")
schema Send("message") { text: string }
@websocket("/ws/chat", "type")
schema Receive("message") { from: string, text: string }
The second argument names the discriminator field inside each JSON message; the tag
in Send("…") / Receive("…") is the value that selects the schema. Send covers
messages you transmit, Receive covers messages from the server.
@sse
@sse("/events/orders", "event")
schema Event("created") { id: string & format("uuid"), total: number & min(0) }
@sse("/events/orders", "event")
schema Event("cancelled") { id: string & format("uuid"), reason: string }
Event("name") is matched against the frame-level event: name of the SSE stream —
not a field inside the data payload. That is the key difference from WebSocket
discriminators.
@grpc
@grpc("UserService.GetUser")
schema Request { id: string & format("uuid") }
@grpc("UserService.GetUser")
schema Response { user: User }
Roles: Request, Response, plus RequestStream and ResponseStream for the
streaming halves of a method.
How matching works at runtime
- Paths. Literal segments match exactly.
{id}(or:id) captures one segment,*matches any one segment,**matches everything after it. Query strings and trailing slashes are ignored. - Status.
Response(404)validates only 404 responses. A bareResponsedefaults to 200 — there is currently no catch-all "any status" response schema, so declare each status you care about. - WebSocket. A message is matched by direction, path, and the discriminator value. Frames whose tag matches no declared schema — and non-JSON text frames — are reported as no schema, which is distinct from matched and failed.
- SSE. Events are matched by path plus the frame's
event:name. - gRPC. Messages are matched by service, method, and direction.
Strictness and extra fields
Missing required fields, null in non-nullable fields, type mismatches, and failed
constraints are errors. Fields present in the payload but not declared in the
schema are flagged as warnings by default — you see the drift without the check
failing. The CLI keeps the same default and adds --schema-strict,
which promotes extra fields to failures; turn it on in CI once a contract is stable.
Unknown formats and unknown constraint names also degrade to warnings, so an
imperfect schema tells you it is imperfect instead of lying in either direction.