Describing schemas
Models
Overview
Models in Taxi describe the data structures that your systems actually work with - the JSON (or other format) returned from API calls, database records, event payloads, and so on.
// This might be the JSON response from GET /api/customers/123
model Customer {
id: CustomerId
firstName: FirstName
lastName: LastName
email: EmailAddress
createdAt: Timestamp
}
What makes Taxi models special is that they’re built from semantic types, which gives meaning to each field. This allows consumers to query for exactly the data they need, regardless of how the original structure is organized.
Consumer Data Contracts
Producers publish models composed of semantic types, but consumers can query for just the data they need using projections. This decouples consumers from the producer's specific model structure.
Basic syntax
// Producer publishes a model
model CustomerRecord {
id: CustomerId
profile: UserProfile
accountStatus: AccountStatus
createdAt: Timestamp
}
// Consumer queries with their own data contract
find { CustomerRecord } as {
customerId: CustomerId
status: AccountStatus
// Only the fields the consumer needs
}
Things to note
- Model fields are composed of semantic types that convey meaning
- The
?
operator marks fields as optional (nullable) - Arrays are declared using
[]
syntax after the type - Comments use
//
for single lines or/* */
for blocks
Parameter and closed models
By default, when you run a TaxiQL query, the query engine will try to construct any model it needs by combining available data and calling services to populate missing fields. This automatic construction happens at query time.
Often this isn’t what you want. The parameter
and closed
modifiers control when and how the TaxiQL engine can construct models.
Default behavior (consumer models)
Without any modifiers, models are “consumer models” - the TaxiQL engine will actively try to construct them:
model CustomerSummary {
id: CustomerId
name: CustomerName
email: EmailAddress
orderCount: OrderCount
}
// During query execution, TaxiQL might:
find { Customer } as CustomerSummary
// 1. Get CustomerId, CustomerName, EmailAddress from Customer
// 2. Call a service to get OrderCount using the CustomerId
// 3. Construct the CustomerSummary with all fields populated
This automatic construction is powerful for data integration, but sometimes inappropriate.
Parameter models
Use parameter
for models that represent input to operations. The TaxiQL engine can only construct these when preparing to call a service:
parameter model CreateCustomerRequest {
firstName: FirstName
lastName: LastName
email: EmailAddress
}
service CustomerService {
operation createCustomer(CreateCustomerRequest): Customer
}
// TaxiQL can construct CreateCustomerRequest, but only to call createCustomer
// It won't try to construct this model for general querying
Parameter models are constructed:
- When TaxiQL needs to call an operation that requires them as input
- From available data that matches the required fields
Parameter models are NOT constructed:
- For general query results or projections
Closed models
Use closed
for models that can only be returned from services, never constructed by combining data:
closed model CustomerRecord {
id: CustomerId // System-generated
name: CustomerName
internalScore: InternalScore // Calculated by the system
lastModified: Timestamp // System-managed
}
service CustomerService {
operation getCustomer(CustomerId): CustomerRecord
}
// TaxiQL will NEVER try to construct CustomerRecord
// It can only get instances by calling getCustomer()
Closed models:
- Can only be obtained by calling services that return them
- Are never constructed by combining fields from other sources
- Prevent unwanted automatic construction of system-managed data
Parameter closed models
Some models serve as both input and output, needing both modifiers:
parameter closed model Customer {
id: CustomerId? // Optional for creates, required for updates
name: CustomerName
email: EmailAddress
}
service CustomerDatabase {
operation insertCustomer(Customer): CustomerId // Customer as parameter
operation updateCustomer(Customer): Customer // Customer as parameter
operation findCustomer(CustomerId): Customer // Customer as closed result
}
// TaxiQL can construct Customer, but only:
// - As input when calling insertCustomer or updateCustomer
// - Never for general querying (closed prevents this)
Model types summary
Model Type | TaxiQL Construction Behavior |
---|---|
Default (consumer) | Constructs freely for queries by combining data and calling services |
parameter | Only constructs when needed as input to operations |
closed | Never constructs - only obtains from service calls |
parameter closed | Only constructs as operation input, never for general queries |
Query-Time Construction
Model construction happens during query execution, not at compile time. The TaxiQL engine decides whether and how to construct models based on these modifiers and the available data sources.
Partial Models
A partial model creates a new type where all fields from an existing model become optional. This is particularly useful for PATCH operations in REST APIs, where you want to update only specific fields of a resource.
Basic Usage
model Person {
name: Name inherits String
age: Age inherits Int
}
partial model PartialPerson from Person
The generated PartialPerson
will have all fields made optional (nullable):
model PartialPerson {
name: Name?
age: Age?
}
Nested types in partials
When a model contains references to other models or arrays, the compiler automatically creates partial versions of those types too:
model Pet {
name: PetName inherits String
age: Age inherits Int
parent: Pet // Self-referential field
}
model Person {
name: Name inherits String
pet: Pet // Reference to another model
deadPets: Pet[] // Array of models
}
partial model PartialPerson from Person
This expands to:
model PartialPerson {
name: Name? // Primitive types become nullable
pet: PartialPet? // Model references become partial and nullable
deadPets: PartialPet[]? // Arrays become nullable arrays of partial types
}
model PartialPet {
name: PetName?
age: Age?
parent: PartialPet? // Self-references become partial too
}
Inheritance and Modifiers
Partial models:
- Do not inherit from their reference type
- Inherit all annotations and modifiers from the reference type
- Can add new annotations and modifiers
- Cannot modify the structure beyond making fields optional
- Cannot be declared as
closed
(the compiler will remove anyclosed
modifiers on the created Partial model)
Example with additional modifiers:
model Person {
name: Name inherits String
}
@OmitNulls
partial parameter model PartialPerson from Person
Field types
Basic fields
model Customer {
// Required fields
id: CustomerId
name: CustomerName
// Optional field
nickname: Nickname?
// Array field
orders: Order[]
// Map field
preferences: Map<PreferenceKey, PreferenceValue>
}
Nested objects
model Order {
orderId: OrderId
// Inline object definition
address: {
street: StreetAddress
city: City
country: CountryCode
}
// Alternative: reference a defined model
shippingAddress: Address
}
Inheritance
While possible, model inheritance should be used sparingly:
model PersonBase {
id: PersonId
firstName: FirstName
lastName: LastName
}
model Employee inherits PersonBase {
employeeId: EmployeeId
department: DepartmentName
}
Model Inheritance
Prefer composition over inheritance for models. Inheritance can create tight coupling between models, which goes against Taxi's principle of loose coupling.
Best practices
Models are typically defined by producing systems - such as REST APIs, databases, or message queues.
The best practices here focus on describing those models using Taxi, rather than the practices of designing good APIs or event payloads.
Use semantic types
// Good - uses semantic types
model Customer {
id: CustomerId
email: EmailAddress
}
// Bad - uses primitive types
model Customer {
id: String // What kind of ID?
email: String // Is this really an email?
}
Document nullability
model User {
// Required fields - core identity
id: UserId
email: EmailAddress
// Optional fields - additional information
middleName: MiddleName?
phoneNumber: PhoneNumber?
}
Organization
// Service-specific models together
model CreateOrderRequest {
customerId: CustomerId
items: OrderItem[]
}
model CreateOrderResponse {
orderId: OrderId
status: OrderStatus
}
// Service definition with its models
service OrderService {
operation createOrder(CreateOrderRequest): CreateOrderResponse
}