| name | serverpod-models |
| description | Define Serverpod data models in YAML (.spy.yaml) — fields, defaults, database tables, relations, enums, exceptions. Use when creating or editing .spy.yaml files. To query the generated ORM instead, use serverpod-database. |
Serverpod Models
Models are defined in .spy.yaml files anywhere under server lib/. They generate Dart classes for server and client, and optionally database tables.
After each change to models, ensure that the code is generated (automatically when a serverpod start is running or manually with serverpod generate). If models with table have changed, the database schema must be updated following the migration workflow.
Basic class
class: Company
fields:
name: String
foundedDate: DateTime?
employees: List<Employee>
Field types: bool, int, double, String, Duration, DateTime, ByteData, UuidValue, Uri, BigInt, generated classes/enums/exceptions, List<T>, Map<K,V>, Set<T>, Record. Use ? for nullable.
Required fields
class: Person
fields:
name: String
nickname: String?, required
Database table
Add table for a database table + ORM:
class: Company
table: company
fields:
name: String
foundedDate: DateTime?
Default values
default= sets the value on both the Dart model and the database column.
defaultModel= sets it only on the Dart model (used when the object is created in Dart).
defaultPersist= sets it only on the database column (requires the field to be nullable in Dart, unless it is the id).
class: Post
table: post
fields:
title: String, default='Untitled'
createdAt: DateTime, default=now
publishedAt: DateTime?, defaultPersist=now
externalId: UuidValue, default=random_v7
views: int, default=0
isPublic: bool, default=false
now (DateTime), random/random_v7 (UuidValue), and quoted literals are supported. A field with a default does not need to be passed to the constructor.
Custom id types
The id field is int with a serial default unless declared explicitly. Declare it to opt into UUID primary keys:
class: Company
table: company
fields:
id: UuidValue?, defaultPersist=random_v7
name: String
defaultPersist=random_v7 — the database generates the id on insert (random for v4).
defaultModel=random_v7 — Dart generates the id when the object is created, so it is known before insert.
id: int?, defaultPersist=serial — the explicit form of the default behavior.
Relations to a model with a custom id type use the same type on the foreign key field.
Scope
- Server-only class:
serverOnly: true
- Per-field:
scope=serverOnly, scope=none (default all)
- Non-persisted field:
!persist (not stored in DB)
- JSON key alias:
jsonKey=display_name
Immutable classes
immutable: true — final fields, ==, hashCode, copyWith.
Inheritance
extends: ParentClass — child inherits parent fields. Only one class in hierarchy has table.
sealed: true — abstract sealed hierarchy for exhaustive subtypes. No table on sealed class.
If parent is serverOnly, children must be too. Children cannot redefine parent fields.
Enums
enum: Status
values:
- pending
- active
- completed
Default serialization: byName. Set serialized: byIndex to use index.
Exceptions
Use exception: instead of class: for serializable exceptions. Same field types as classes; supports default and defaultModel. Uncaught exceptions become generic 500 errors on the client; only serializable exceptions send their data.
exception: MyException
fields:
message: String
errorType: MyEnum
Throw on server, catch on client:
// Server
throw MyException(message: 'Failed', errorType: MyEnum.thingyError);
// Client
try {
await client.example.doThingy();
} on MyException catch (e) {
print(e.message);
}
Serializable exceptions can also be sent over streams (both directions; the stream closes after). Do not put sensitive data in exception fields — they are sent to the client.
Indexes
indexes:
company_name_idx:
fields: name
unique: true
nulls_distinct: false
Field-level unique auto-generates a btree unique index:
fields:
tenantId: int
category: String
email: String, unique
value: String, unique(per=category)
amount: int, unique(per=[category, tenantId])
Relations
One-to-one (FK with unique index):
- ID field:
addressId: int, relation(parent=address) + unique index on addressId
- Object field:
address: Address?, relation (generates addressId)
- Optional:
relation(optional) for nullable FK; relation(field=customId) for custom FK name
- Bidirectional: same
relation(name=...) on both sides; fk on the FK side (or field= for a custom FK name)
One-to-many:
- "One" side:
employees: List<Employee>?, relation
- "Many" side:
companyId: int, relation(parent=company) (no unique index)
- Bidirectional:
relation(name=company_employees) on both sides
Many-to-many: Use a join table model with two relation fields.
Referential actions: relation(onDelete=Cascade) and relation(onUpdate=...) map to the SQL foreign key actions (Cascade, SetNull, SetDefault, Restrict, NoAction). SetNull requires a nullable foreign key (a nullable object relation field, or relation(optional)). Add deferrable to allow constraint checks to be deferred explicitly, or deferred to defer them by default until commit. Restrict actions are always checked immediately, even on a deferred constraint; use NoAction when the check must be deferrable.
Querying: include for eager loading, includeList with where/orderBy/limit/offset for list relations. attach/detach for managing relations.
Backward compatibility
To keep backward compatibility, do not change or remove fields in serialized classes used by clients. Add new fields only if nullable or with a default value, so older clients that don't send the field still work.
Reference files