Cerial
Inheritance

Extends

Schema-level inheritance for models, objects, tuples, enums, and literals.

Cerial supports schema-level inheritance across all five type kinds — models, objects, tuples, enums, and literals. A child type inherits the fields (or elements, values, variants) from a parent type and can add its own. Inheritance is resolved at compile time: generators see a fully flattened result with no extends references remaining.

Syntax

Three forms are available:

// Full inherit — gets everything from Parent
model Child extends Parent {
  extraField String
}

// Pick — only the listed fields
model Subset extends Parent[field1, field2] {
  myField Int
}

// Omit — everything except the listed fields
model Without extends Parent[!field1, !field2] {
  myField Int
}
FormSyntaxEffect
Full inheritextends ParentAll parent fields/elements/values
Pickextends Parent[a, b]Only the listed fields
Omitextends Parent[!a, !b]Everything except the listed fields

Pick and omit cannot be mixed in the same declaration — you use one or the other per extends clause.

If the filter gives you everything you need, the child body can be empty:

enum CoreRole extends BaseRole[Admin, User] { }
tuple FirstTwo extends BasePair[0, 1] { }

Empty filter brackets (extends Parent[]) are an error. Use extends Parent to inherit everything, or list specific fields inside the brackets.

Model Inheritance

Models can extend abstract models to inherit fields. The child gets its own table, its own generated types, and its own client accessor. Both concrete and abstract models can only extend abstract models — extending a concrete model is not allowed.

abstract model BaseEntity {
  id Record @id
  createdAt Date @createdAt
  updatedAt Date @updatedAt
}

model User extends BaseEntity {
  email Email @unique
  name String
  age Int?
}

User ends up with five fields: id, createdAt, updatedAt, email, name, and age. The generated types reflect the full flattened set:

interface User {
  id: CerialId<string>;
  createdAt: Date;
  updatedAt: Date;
  email: string;
  name: string;
  age: number | undefined;
}

Relations in Inherited Models

Relations and their backing Record fields inherit normally. You can also add new relations in the child body:

abstract model BaseEntity {
  id Record @id
  createdAt Date @createdAt
}

model BlogPost extends BaseEntity {
  title String
  authorId Record
  author Relation @field(authorId) @model(BlogAuthor)
}

When using omit, you can exclude relation fields along with their backing Record fields:

model PostSummary extends BlogPost[!author, !authorId] {
  summary String?
  wordCount Int?
}

PostSummary inherits id, createdAt, and title but not the relation or FK.

Object Inheritance

Objects can extend other objects to inherit sub-fields:

object BaseAddress {
  street String
  city String
  zip String
  country String @default('US')
}

object DetailedAddress extends BaseAddress {
  apartment String?
  coordinates Float[]
}

DetailedAddress has six fields: street, city, zip, country, apartment, and coordinates. Use the extended object on model fields as usual:

model Store {
  id Record @id
  name String
  address DetailedAddress
}

Tuple Inheritance

Tuples append new elements after the parent's elements:

tuple Pair { String, Int }
tuple Triple extends Pair { Bool }

Triple is [String, Int, Bool]. Named elements work the same way:

tuple NamedPair { name String, age Int }
tuple NamedTriple extends NamedPair { active Bool }

NamedTriple is [name: String, age: Int, active: Bool].

Named Element Override

If a child declares a named element that matches a parent's named element, the child's version replaces the parent's at the same position — it doesn't append:

tuple Base { label String, count Int }
tuple Override extends Base { count Float }

Override is [label: String, count: Float] — the child's count Float replaced the parent's count Int at position 1, rather than being appended as a third element.

Index-Based Pick and Omit

Tuples use zero-based indices instead of field names for pick and omit:

tuple Base { String, Int, Bool }

tuple FirstTwo extends Base[0, 1] { }
// [String, Int]

tuple WithoutSecond extends Base[!1] { }
// [String, Bool]

Enum Inheritance

Enums inherit values from a parent enum and can add new ones:

enum BaseRole { Admin, User, Moderator }
enum ExtendedRole extends BaseRole { SuperAdmin, Guest }

ExtendedRole contains all five values: Admin, User, Moderator, SuperAdmin, Guest.

Pick and omit work with value names:

enum CoreRole extends BaseRole[Admin, User] { }
// Admin, User

enum NonAdminRole extends BaseRole[!Admin] { }
// User, Moderator

Literal Inheritance

Literals inherit variants from a parent literal and can add new ones:

literal BasePriority { 'low', 'medium', 'high' }
literal ExtendedPriority extends BasePriority { 'critical', 'urgent' }

ExtendedPriority generates 'low' | 'medium' | 'high' | 'critical' | 'urgent'.

Pick and omit work with variant values. For boolean variants, use the value directly:

literal Mixed { 'active', 'inactive', true, false }
literal StringOnly extends Mixed[!true, !false] { }
literal BoolOnly extends Mixed[true, false] { }

Numeric literal variants use the number directly in pick/omit:

literal Level { 1, 2, 3 }
literal ExtendedLevel extends Level { 4, 5 }

Field Override

A child can redefine a field that it inherits. The child's definition replaces the parent's:

abstract model BaseUser {
  id Record @id
  email Email @unique
  name String
  role String @default('user')
}

model Admin extends BaseUser {
  role String @default('admin')   // overrides parent's default
  level Int @default(1)
  permissions String[]
}

If the parent marks a field as !!private, it cannot be overridden — the child can still inherit it, and can omit it with pick/omit syntax, but redefining it in the child body is an error.

Multi-Level Chains

Inheritance chains can go as deep as needed. Each level flattens the full chain:

abstract model L1Base {
  id Record @id
  createdAt Date @createdAt
}

abstract model L2Named extends L1Base {
  name String
  description String?
}

abstract model L3Tagged extends L2Named {
  tags String[]
  metadata Int?
}

model Concrete extends L3Tagged {
  status String @default('active')
}

Concrete gets all fields from every level: id, createdAt, name, description, tags, metadata, and status.

To share fields between concrete models, extract common fields into an abstract intermediary:

abstract model BaseEntity {
  id Record @id
  createdAt Date @createdAt
  updatedAt Date @updatedAt
}

abstract model BaseUser extends BaseEntity {
  email Email @unique
  name String
  isActive Bool @default(true)
}

model RegularUser extends BaseUser {
  preferences String?
}

model Admin extends BaseUser[!isActive] {
  level Int @default(1)
  permissions String[]
}

Both RegularUser and Admin inherit the shared fields. Admin omits isActive since admins are always active by definition.

Cross-File Extends

Types can extend parents defined in other .cerial files — no import syntax is needed. Cerial resolves all types across files within a schema entry before applying inheritance:

abstract model BaseEntity {
  id Record @id
  createdAt Date @createdAt
}
model User extends BaseEntity {
  email Email @unique
  name String
}

Both files must be in the same schema folder (or same schema entry in a multi-schema config). Cross-schema extends — between different schema entries — is not supported.

Rules and Restrictions

RuleDetail
Single parent onlyextends A, B is not allowed. One parent per type.
Same kind onlyA model can only extend a model, an object can only extend an object. No cross-kind extends.
Same schema entryBoth parent and child must belong to the same schema entry (folder/config group).
Abstract models onlyOnly models can be declared abstract. Objects, tuples, enums, and literals cannot be abstract.
Models extend abstractAll models (concrete and abstract) can only extend abstract models. Extending a concrete model is forbidden.
No circular referencesA type cannot extend itself or create a circular chain (A extends B extends A).
!!private fieldsPrivate fields are inherited but cannot be overridden in the child body. They can be omitted via pick/omit.
Pick/omit not mixedA single extends clause uses either pick or omit — not both.
Empty filter is an errorextends Parent[] with no fields listed is invalid. Use extends Parent for full inherit.

Post-Resolution Validation

After inheritance is resolved, Cerial validates that the result is still well-formed:

  • No empty types — If pick/omit removes all fields, elements, values, or variants, the resulting type is empty and Cerial reports an error.
  • Concrete models need @id — Every concrete model must have an @id field after resolution. If a child picks or omits fields such that @id is lost, that's an error.

Abstract models don't need @id since they don't generate tables — but any concrete model inheriting from them must end up with one.

On this page