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
}| Form | Syntax | Effect |
|---|---|---|
| Full inherit | extends Parent | All parent fields/elements/values |
| Pick | extends Parent[a, b] | Only the listed fields |
| Omit | extends 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, ModeratorLiteral 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
| Rule | Detail |
|---|---|
| Single parent only | extends A, B is not allowed. One parent per type. |
| Same kind only | A model can only extend a model, an object can only extend an object. No cross-kind extends. |
| Same schema entry | Both parent and child must belong to the same schema entry (folder/config group). |
| Abstract models only | Only models can be declared abstract. Objects, tuples, enums, and literals cannot be abstract. |
| Models extend abstract | All models (concrete and abstract) can only extend abstract models. Extending a concrete model is forbidden. |
| No circular references | A type cannot extend itself or create a circular chain (A extends B extends A). |
!!private fields | Private fields are inherited but cannot be overridden in the child body. They can be omitted via pick/omit. |
| Pick/omit not mixed | A single extends clause uses either pick or omit — not both. |
| Empty filter is an error | extends 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@idfield after resolution. If a child picks or omits fields such that@idis 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.