Decorators
Built-in validation and metadata decorators.
Decorators attach metadata to models, fields, enum variants, and operations. They are the main way to influence what the code generators produce. This page lists every built-in decorator and exactly what each generator does with it.
For decorator syntax (placement, argument forms), see Language Reference → Decorators.
How decorators are consumed
A decorator is metadata; it does nothing on its own. Each generator reads the
decorators it understands and ignores the rest. So @sql.type("JSONB") affects
only the SQL output, and @format("email") affects only JSON Schema. This means
you can annotate a field for several targets at once:
model Account {
@format("email") @sql.type("CITEXT") @unique
1 email: string
}
Constraint and key decorators
These describe database-style constraints. They drive SQL output and are tracked by the compatibility checker.
| Decorator | Argument | Effect |
|---|---|---|
@primaryKey |
none | SQL: PRIMARY KEY. Diff: changing it is breaking (rule R015). |
@unique |
none | SQL: UNIQUE. Diff: adding it is breaking for writers (R012); removing it is safe (R014). |
@default(expr) |
an expression | SQL: DEFAULT <expr>. A field added with a default is a safe change (R002) rather than a breaking one. |
@check(expr) |
a boolean expression | SQL: CHECK (<expr>). Diff: adding is breaking for writers (R013); removing is safe (R014). |
@references(Type.field) |
a qualified field | SQL: REFERENCES table(column). Diff: changing the target is breaking (R018). |
@table("orders")
model Order {
@primaryKey @default(gen_uuid())
1 id: uuid
@references(Customer.id)
2 customerId: uuid
@check(total >= 0)
3 total: decimal(12, 2)
}
The argument to @default and @check is a real expression: literals
(0, "draft"), function calls (gen_uuid(), now()), comparisons
(total >= 0), and boolean logic (a && b) are all parsed and rendered into
the target SQL.
Validation decorators
These describe value constraints. They drive JSON Schema output (where they map to standard keywords) and are available to any future validator.
| Decorator | Argument | JSON Schema keyword |
|---|---|---|
@format(name) |
string | format (on string fields) |
@minValue(n) |
number | minimum |
@maxValue(n) |
number | maximum |
@minLength(n) |
number | minLength |
@maxLength(n) |
number | maxLength |
@pattern(re) |
string | pattern |
model Profile {
@format("email")
1 email: string
@minLength(2) @maxLength(2) @format("iso3166-1-alpha2")
2 country: string
@minValue(0) @maxValue(120)
3 age: i32
@pattern("^[A-Z]{3}$")
4 code: string
}
Generated JSON Schema for age:
{ "type": "integer", "minimum": 0, "maximum": 120 }
@format values are free-form strings. Standard ones (email, uri, uuid,
date-time) are understood by JSON Schema validators; custom ones
(iso4217, e164) are passed through for your own tooling.
Storage and mapping decorators
These override how a field maps to a specific target.
| Decorator | Argument | Effect |
|---|---|---|
@table(name) |
string | SQL: use this table name instead of the snake-cased model name. |
@sql.type(type) |
string | SQL: use this exact column type, overriding the default mapping. |
@sql.column(name) |
string | SQL: use this column name instead of the snake-cased field name. |
@table("user_accounts")
model UserAccount {
@sql.column("user_id") @primaryKey
1 id: uuid
@sql.type("JSONB")
2 preferences: string
}
CREATE TABLE user_accounts (
user_id UUID NOT NULL PRIMARY KEY,
preferences JSONB NOT NULL
);
Lifecycle decorators
| Decorator | Applies to | Effect |
|---|---|---|
@deprecated |
field, enum variant | Marks the element deprecated. Diff: adding @deprecated to a field is reported as a warning (R019). |
@compatibility(mode) |
model | Documents the intended compatibility mode (backward, forward, full, none). Enforce it with openschema check --mode <mode>. |
@compatibility(backward)
model Order {
1 id: uuid
@deprecated
2 legacyRef: string?
}
Visibility decorators
Visibility controls which fields appear in operation inputs (request bodies, mutation arguments) versus outputs (responses). This lets one model serve both without leaking server-managed or write-only fields. Modeled on TypeSpec visibility.
| Decorator | Argument | Effect |
|---|---|---|
@visibility(...phases) |
one or more of "read", "create", "update", "delete", "query" |
The field appears only in the listed lifecycle phases. |
@invisible |
none | The field never appears in any operation input or output. |
A field with no visibility decorator appears everywhere. The phases group into two operation contexts:
- Output (responses) shows fields visible in
read. - Input (mutation arguments / request bodies) shows fields visible in
create,update, orquery.
model User {
@visibility("read") // server-generated: returned, never sent
1 id: uuid
@visibility("create", "read") // set on create, returned
2 name: string
@visibility("create", "update") // write-only: never returned (e.g. a secret)
3 password: string
@invisible // internal: never in any API surface
4 fraudNotes: string?
}
@mutation op createUser(user: User): User
Generated GraphQL keeps id/name in the output type User and name/password
in the input UserInput — id is omitted from input (you don't send it on
create), password from output, and fraudNotes from both:
type User {
id: UUID!
name: String!
}
input UserInput {
name: String!
password: String!
}
Visibility is target-agnostic: a future HTTP/OpenAPI generator applies the same input/output filtering to request and response schemas.
Operation routing decorators
These appear on op declarations and route them to GraphQL root types.
| Decorator | GraphQL root |
|---|---|
@query |
type Query |
@mutation |
type Mutation |
@subscription |
type Subscription |
@query op getOrder(id: uuid): Order
@mutation op placeOrder(order: Order): Order
An operation with no routing decorator defaults to Query. See
Code Generation → GraphQL.
HTTP decorators
These appear on op declarations and drive the OpenAPI generator.
| Decorator | Argument | Effect |
|---|---|---|
@get @post @put @delete @patch |
none | Sets the HTTP method for the operation. |
@route(path) |
string | Sets the URL path. {name} placeholders become path parameters bound to the operation's parameter of the same name. |
@get @route("/orders/{id}")
op getOrder(id: uuid): Order
@post @route("/orders")
op placeOrder(order: Order): Order
- A parameter named in the route's
{...}becomes a path parameter. - For
GET, the remaining parameters become query parameters. - For
POST/PUT/PATCH, the remaining parameters form the request body. - With no method decorator, an
opdefaults toGET(orPOSTif it also has@mutation); with no@route, the path defaults to/opName.
See Code Generation → OpenAPI.
Defining your own decorators
The language does not restrict decorator names — any @name or @ns.name with
valid arguments parses. Unrecognized decorators are simply carried in the AST
and ignored by current generators, so you can annotate schemas for your own
tooling today and add a generator that reads them later.