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
Type | Description |
---|---|
Boolean | Represents a value which is either true or false |
Numeric
Type | Description |
---|---|
Int | A signed integer - a whole number (positive or negative), with no decimal places |
Long | A signed long - a whole number (positive or negative), with no decimal places |
Decimal | A signed decimal number - a whole number with decimal places |
Double | A double-precision 64-bit IEEE 754 floating point number |
Text
Type | Description |
---|---|
String | A 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.
Type | Description | Default Format | Example |
---|---|---|---|
Date | A date, without time or timezone | yyyy-MM-dd | 2024-03-15 |
Time | Time only, excluding the date part | HH:mm:ss | 14:30:00 |
DateTime | A date and time, without timezone | yyyy-MM-dd'T'HH:mm:ss.SSS | 2024-03-15T14:30:00.000 |
Instant | A point in time with timezone | yyyy-MM-dd'T'HH:mm:ss[.SSS]X | 2024-03-15T14:30:00Z |
Format Symbols:
Symbol | Meaning | Example |
---|---|---|
y | Year | 2024 |
M | Month | 07 or July |
d | Day | 25 |
H | Hour (0-23) | 13 |
m | Minute | 30 |
s | Second | 45 |
S | Millisecond | 678 |
X | Timezone (accepts Z) | Z or +01:00 |
Z | RFC 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>
}
Array values can be read using a get function:
Taxi’s stdlib has a number of functions for filtering and reading arrays, such as:
Maps
Maps are key-value collections:
type Inventory inherits Map<ProductId, Quantity>
Using Collections
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:
- 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
- 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
- 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 tostream {}
queries:find { A | B }
- Load A and B, return if either produce a resultfind { 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"
matchesCountry.NZ
- Case-insensitive value matching:
"new zealand"
matchesCountry.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 toCountry.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" })
}