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 TypeTaxiQL Construction Behavior
Default (consumer)Constructs freely for queries by combining data and calling services
parameterOnly constructs when needed as input to operations
closedNever constructs - only obtains from service calls
parameter closedOnly 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 any closed 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
}
Previous
Semantic types
Next
Services