.zn DSL Syntax
An alternative to the TypeScript builder — a clean, structured language for defining schemas. Both approaches compile to the exact same runtime graph.
When to use the DSL
The TypeScript builder is great for programmatic control — loops, conditionals, custom functions. The .zn DSL is better when you want readability above all else. Teams with non-TypeScript developers, or schemas that are mostly straightforward, benefit from the cleaner syntax.
TypeScript builder
- ·Full programmatic control
- ·IDE autocomplete built-in
- ·Can use loops, conditions, functions
- ·Best for complex/dynamic schemas
.zn DSL
- ·Clean, readable syntax
- ·Less noise, more signal
- ·Structured blocks (fields, relations, indexes)
- ·Best for straightforward schemas
Complete example
/// Application usersmodel User { table "users" comment "Application users" meta { module = "auth" owner = "platform-team" } fields { id uuid primary default(uuid()) createdAt datetime default(now()) updatedAt datetime autoUpdate /// Login email address email string unique /// Display name name string? role Role default(USER) settings json? } relations { posts hasMany Post { foreignKey authorId } } indexes { index(email) } constraints { unique(email, name) }}Model structure
Every model is a block with named sections. Each section has a clear purpose:
| Block | Purpose | Example |
|---|---|---|
table "name" | Custom PostgreSQL table name | table "users" |
comment "text" | Model documentation | comment "Application users" |
meta { ... } | Key-value metadata | module = "auth" |
fields { ... } | Column definitions | email string unique |
relations { ... } | Foreign key relationships | author belongsTo User { ... } |
indexes { ... } | Database indexes | index(email, createdAt) |
constraints { ... } | Unique/check constraints | unique(email, name) |
Field syntax
Each field is a single line: name type modifiers...
id uuid primary default(uuid()) // PK with auto UUIDemail string unique // unique stringname string? // nullable (? suffix)age int default(0) // integer with defaultscore float // decimal numberactive boolean default(true) // booleancreatedAt datetime default(now()) // timestamp with defaultupdatedAt datetime autoUpdate // auto-update on changebio text? // long text, nullabledata json? // JSONB columntags string[] // array column ("Type[]")role Role default(USER) // enum field with default valueBuilt-in scalar types: uuid, string, text, int, bigint, float, decimal, boolean, datetime, json, bytes. Any other identifier is treated as an enum or model name.
Field modifiers
| Modifier | Effect |
|---|---|
primary | Marks the field as primary key. |
unique | UNIQUE constraint on the field. |
autoUpdate | Auto-set to now() on every UPDATE. |
default(value) | Default value — now(), uuid(), autoincrement(), literals (0, true, "USER"), or an enum identifier (USER). |
map("col") | Custom database column name. |
comment("text") | Per-field doc comment that lands in the schema graph metadata. |
name("snake") | Alternative to map(...) — for places where naming an index/constraint matters. |
? | Make the field nullable (suffix on the type). |
[] | Array column (suffix on the type). |
Enum syntax
Enums are declared at the top level of a file (outside any model block) and referenced from fields by name.
/// Application user rolesenum Role { ADMIN USER MODERATOR} model User { fields { role Role default(USER) }}Relation syntax
Relations are explicit blocks with configuration:
relations { // belongsTo — this model has the FK author belongsTo User { field authorId references id onDelete cascade } // hasMany — other model has the FK posts hasMany Post { foreignKey authorId } // manyToMany — through junction tags manyToMany Tag { through PostTag fromField postId toField tagId }}Keyword reference
| Keyword | Meaning |
|---|---|
model Name { … } | Define a table. |
enum Name { V1 V2 … } | Define an enum. |
table "name" | Custom database table name. |
comment "text" | Model-level doc comment. |
meta { key = value } | Arbitrary key-value metadata. |
fields { … } | Field declarations block. |
relations { … } | Relation declarations block. |
indexes { … } | Index declarations block — index(f1, f2) name("idx"). |
constraints { … } | Constraint declarations block — unique(f1, f2) name("uq"). |
primary / unique / autoUpdate | Bare field modifiers. |
default(…) / map(…) / comment(…) / name(…) | Function-call field modifiers. |
Type? / Type[] | Nullable / array suffix on a field type. |
/// | Doc comment — survives into the schema graph. |
import "path" | Pull another .zn / .zanith file into the same compilation unit. |
Imports
Split your schema across files and import them by path. Imports are whole-file — every model and enum declared in the imported file becomes available. There is no destructured import syntax.
import "../enums/role.zn"import "../auth/user.zn" model Post { table "posts" fields { id uuid primary default(uuid()) title string } relations { author belongsTo User { field authorId references id } }}