# Serverpod Models

> 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.

- Skill: `serverpod/serverpod-models` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add serverpod/serverpod-models`
- Raw SKILL.md: https://api.skillmd.com/api/skills/serverpod/serverpod-models/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: serverpod (https://skillmd.com/u/serverpod)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/serverpod/serverpod-models

---


# 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](../serverpod-migrations/SKILL.md).

## Basic class

```yaml
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

```yaml
class: Person
fields:
  name: String
  nickname: String?, required
```

## Database table

Add `table` for a database table + ORM:

```yaml
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`).

```yaml
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:

```yaml
class: Company
table: company
fields:
  id: UuidValue?, defaultPersist=random_v7  # database generates the id
  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

```yaml
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.

```yaml
exception: MyException
fields:
  message: String
  errorType: MyEnum
```

Throw on server, catch on client:

```dart
// 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

```yaml
indexes:
  company_name_idx:
    fields: name
    unique: true
    nulls_distinct: false  # PostgreSQL only, treats NULLs as equal
```

Field-level `unique` auto-generates a btree unique index:

```yaml
fields:
  tenantId: int
  category: String
  # single-column unique index
  email: String, unique
  # composite unique index on (category, value)
  value: String, unique(per=category)
  # composite unique index on (category, tenantId, value)
  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

- [`references/client-side-database.md`](references/client-side-database.md) — the `database:` keyword, generating tables on the client.
- [`references/custom-serialization.md`](references/custom-serialization.md) — using hand-written or Freezed classes as model types via `extraClasses`.

