Describing schemas

Basic Types

Type Hierarchy

Type System Foundation

Every type in Taxi inherits from `Any`, which in turn inherits from `Nothing`. This creates a complete type hierarchy that enables type-safe operations across the system.

Core Primitive Types

Logical

TypeDescription
BooleanRepresents a value which is either true or false

Numeric

TypeDescription
IntA signed integer - a whole number (positive or negative), with no decimal places
LongA signed long - a whole number (positive or negative), with no decimal places
DecimalA signed decimal number - a whole number with decimal places
DoubleA double-precision 64-bit IEEE 754 floating point number

Text

TypeDescription
StringA collection of characters

Date and Time Types

Time Handling

Taxi provides several types for handling dates and times. When dealing with points in time, prefer `Instant` as it includes timezone information.
TypeDescriptionDefault FormatExample
DateA date, without time or timezoneyyyy-MM-dd2024-03-15
TimeTime only, excluding the date partHH:mm:ss14:30:00
DateTimeA date and time, without timezoneyyyy-MM-dd'T'HH:mm:ss.SSS2024-03-15T14:30:00.000
InstantA point in time with timezoneyyyy-MM-dd'T'HH:mm:ss[.SSS]X2024-03-15T14:30:00Z

Format Symbols:

SymbolMeaningExample
yYear2024
MMonth07 or July
dDay25
HHour (0-23)13
mMinute30
sSecond45
SMillisecond678
XTimezone (accepts Z)Z or +01:00
ZRFC 822 timezone+0100

Example Usage:

// Define types with specific formats
@Format("dd/MM/yyyy")
type BirthDate inherits Date

@Format("yyyy-MM-dd HH:mm")
type AppointmentTime inherits DateTime

@Format("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
type EventTimestamp inherits Instant

Collection Types

Arrays

Arrays can be declared using either bracket notation or generic syntax:

model Person {
  // These declarations are equivalent
  friends : Person[]
  alsoFriends : Array<Person>
}

Maps

Maps are key-value collections:

type Inventory inherits Map<ProductId, Quantity>

Using Collections

  • Use arrays for ordered collections of the same type
  • Only use maps when you need dynamic key-value relationships
  • Consider creating a proper model instead of a map if the structure is known

Union and Intersection types

Taxi has partial (but growing) support for union and intersection types.

Theses are most commonly found in queries, specifically relating to joining streams.

Merging properties

For both Union Types and Intersection Types inherit all the properties of their underlying types:

model Person { 
  name : Name
}
model LivingThing {
   age : Age
}
type PersonOrAge = Person | Age
type PersonWithAge = Person & Age

In both scenarios, the resulting type contains two fields:

  • name : Name
  • age : Age

Dealing with name conflicts

If both types declare a type with the same name, this results in a conflict. Conflicts are resolved as follows:

  1. If both types have the same name and the same type, the resulting type will contain only a single field.
model Person { 
  name : Name
}
model LivingThing {
   name : Name
}
type PersonWithAge = Person & Age // contains only name : Name
  1. If both models have fields with the same name, but different types, the declaring model’s name is prepended:
model Tweet {
   id : TweetId inherits Int
   user : UserId
}
model User {
   id : UserId inherits Int
}

// Contains fields Tweet_id and User_id
type TweetAndUser = Tweet | User
  1. If both models have fields with the same name, and both models also have the same name, then the declaring models fully qualified name is prepended:
namespace foo {
  model Tweet {
     id : TweetId inherits Int
     user : UserId
  }
}

namespace bar {
  model Tweet {
     id : UserId inherits Int
  }
}
// Contains fields foo_Tweet_id and bar_Tweet_id
type TweetAndUser = Tweet | User
Unlike in other languages, fields names are a lot less important in Taxi. Most mapping, orchestration, and transformation functions use Types, rather than field names.

Union types

A union type is declared using the | operator, and broadly represents “This or That”

// Subscribe to feeds of both Foo and Bar, and emit
// when either emit a message.
stream { Foo | Bar }

Intersection types

Intersection types are declared using the & operator, and broadly represent “This and That”

// Subscribe to feeds of both Foo and Bar, and emit
// only after both have emitted a message
stream { Foo & Bar }

A stream of an intersection type is generally stateful, so requires some form of State Store provided by the TaxiQL engine. Read more about this in State Stores.

Limitations

Currently, Union and Intersection types are only supported as discovery types in a stream - as shown above.

Over time, support will expand to cover scenarios like:

  • Support for declaring top-level union / intersection types:
// This is currently legal, but not fully supported by query engines
type Baz = Foo & Bar
type BreadRoll = Foo | Bar
  • Fields with Union Types (similar to OpenAPI’s anyof)
  • find { } queries, where the behaviour is similar to stream {} queries:
    • find { A | B } - Load A and B, return if either produce a result
    • find { A & B } - Load A and B, return only if both produce a result

Special Types

Nothing

Nothing is a special type indicating that a function or expression never returns normally, typically due to throwing an exception.

As the bottom type in the type hierarchy, Nothing is a subtype of all other types. This allows Nothing expressions to be assigned to any type, useful for handling exceptional cases.

Characteristics:

  • No Instances: Represents the absence of a value
  • Bottom Type: Subtype of all types, enabling flexible type assignments
  • Control Flow: Used for functions that do not complete normally

Any

Any is the root type in the Taxi type system, representing the most general type. All other types inherit from Any, making it the universal supertype.

Characteristics:

  • Universal Supertype: All types inherit from Any
  • Extensibility: Provides a common base for defining more specific semantic subtypes

Using Any

While `Any` is available, prefer defining specific semantic types that convey meaning. `Any` should primarily serve as the foundation for more specific types.

Void

Represents the absence of a return value in operations that don’t return anything.

Type Nullability

By default, all types are non-nullable. Use the ? operator to make a type nullable:

model Person {
    // Required fields
    id : PersonId
    name : PersonName
    
    // Optional fields
    nickname : Nickname?
    middleName : MiddleName?
}

Nullability Enforcement

Taxi defines nullability in the schema, but enforcement is handled by the implementing systems and tools.

Enums

Enums in Taxi allow you to define a fixed set of values. Values can be explicitly provided or inferred from the enum name.

// Basic enum - values are same as names
enum BookCategory {
    FICTION,
    NON_FICTION
}

// Enum with explicit values
enum Country {
    NEW_ZEALAND("NZ"),
    AUSTRALIA("AUS"),
    UNITED_KINGDOM("UK")
}

Value Types and Inference

Taxi automatically infers the enum’s base type based on its values:

// Inferred as Int
enum Numbers {
   One(1),
   Two(2)
}

// Inferred as String when mixing types
enum Mixed {
   One("One"),
   Two(2)    // Will be converted to string
}

// Boolean values
enum Selected {
   Yes(true),
   No(false)
}

// String booleans as members
enum Flags {
   `true`,    // Note the backticks
   `false`
}

Type Inference

When values are mixed (e.g., strings and numbers), Taxi defaults to treating all values as strings.

Generic Enums with Object Bodies

Enums can hold structured data using generic type parameters:

model ErrorDetails {
   code : ErrorCode inherits Int
   message : ErrorMessage inherits String
}

enum Errors<ErrorDetails> {
   BadRequest({ code : 400, message : 'Bad Request' }),
   Unauthorized({ code : 401, message : 'Unauthorized' })
}

You can access properties of enum values:

model Response {
  // Access a property of an enum value
  errorCode: ErrorCode by Errors.BadRequest.code
  
  // Access nested properties
  errorMessage: ErrorMessage by Errors.BadRequest.message
}

Generic Constraints

- Enums support at most one type argument - Object values must match the specified type exactly - All required fields must be provided

Lenient Matching

Basic Lenient Matching

lenient enum Country {
   NZ("New Zealand"),
   AUS("Australia")
}

This allows:

  • Case-insensitive name matching: "nz" matches Country.NZ
  • Case-insensitive value matching: "new zealand" matches Country.NZ

Special Characters in Lenient Matching

lenient enum DayCountConvention {
   ACT_360("ACT/360")
}

This matches:

  • "Act/360"
  • "ACT/360"
  • "act/360"

Default Values

Enums can specify a default value for unmatched inputs:

enum Country {
   NZ("New Zealand"),
   AUS("Australia"),
   default UNKNOWN("Unknown")
}

Default values work with both names and values:

  • "UK" resolves to Country.UNKNOWN
  • Can be combined with lenient matching
  • Only one default value is allowed per enum

Synonyms

Basic Synonyms

enum English {
   One,
   Two
}

enum French {
   Un synonym of English.One,
   Deux synonym of English.Two
}

Multiple Synonyms

enum English { One }
enum French { Un }
enum Australian {
   One synonym of [English.One, French.Un]
}

Synonym Characteristics

- Relationships are bidirectional - Synonyms are transitive across multiple enums - Can use fully qualified names - Can reference synonyms before their target is declared

Array Support

Enums can be used in array literals:

enum Country {
   NZ("NZD"),
   AU("AUD")
}

// Arrays can contain enum values
given { countries: Country[] = ["NZD", "AUD"] }

// Arrays can use enum names
given { countries: Country[] = ["NZ", "AU"] }

// Arrays can use enum references
given { countries: Country[] = [Country.NZ, Country.AU] }

Enums - common Patterns

1. Status Enums

enum Status<StatusDetails> {
   ACTIVE({ code: "A", description: "Active" }),
   INACTIVE({ code: "I", description: "Inactive" }),
   default UNKNOWN({ code: "U", description: "Unknown" })
}

2. Code Mappings

enum ExternalMapping {
   INTERNAL_A("EXT_1") synonym of Internal.A,
   INTERNAL_B("EXT_2") synonym of Internal.B
}

3. Validation Results

model ValidationDetails {
   severity: Severity inherits String
   code: Code inherits Int
   message: Message inherits String
}

enum ValidationResult<ValidationDetails> {
   OK({ severity: "INFO", code: 0, message: "Valid" }),
   ERROR({ severity: "ERROR", code: 1, message: "Invalid" })
}
Previous
Welcome to Taxi
Next
Semantic types