Skip to main content

Schemas

Schemas define data shapes for validation and pattern matching. They're used to validate responses, route data based on structure, and document expected data formats.

Basic syntax

schema SchemaName {
field: type,
optionalField: type?,
nestedField: {
subField: type
}
}

Field types

TypeDescriptionExample
stringText value"hello"
numberNumeric value42, 3.14
booleanTrue or falsetrue, false
dateDate/datetime"2024-01-20"
arrayArray of values[1, 2, 3]
objectNested object{ a: 1 }
anyAny typeanything
nullNull valuenull

Optional fields

Use ? suffix for optional fields:

schema User {
id: string,
name: string,
email: string?,
phone: string?
}

Typed arrays

Specify array element types:

schema UserList {
users: array<User>,
total: number
}

schema Order {
id: string,
items: array<{
productId: string,
quantity: number,
price: number
}>
}

Nested schemas

Define complex nested structures:

schema Invoice {
id: string,
customer: {
id: string,
name: string,
address: {
street: string,
city: string,
country: string
}
},
lineItems: array<{
description: string,
amount: number
}>,
total: number
}

Schema references

Reference other schemas:

schema Address {
street: string,
city: string,
postalCode: string,
country: string
}

schema Customer {
id: string,
name: string,
billingAddress: Address,
shippingAddress: Address?
}

Using schemas for validation

Validate data against schemas:

action ValidateResponse {
get "/users"

for user in response.users {
validate user {
assume .id is string,
assume .name is string,
assume .email is string
}
store user -> validUsers { key: .id }
}
}

Using schemas for pattern matching

Route data based on schema matches:

schema SuccessResponse {
data: any,
status: string
}

schema ErrorResponse {
error: string,
code: number
}

schema RateLimitResponse {
error: string,
retryAfter: number
}

action HandleResponse {
get "/data"

match response {
SuccessResponse -> store response.data -> data { key: .id },
RateLimitResponse -> retry { delay: response.retryAfter * 1000 },
ErrorResponse -> abort response.error,
_ -> abort "Unknown response format"
}
}

Schema matching rules

Schemas match when:

  1. All required fields are present
  2. Field types match
  3. Optional fields, if present, match their types
schema StrictUser {
id: string, // Required
name: string, // Required
email: string? // Optional
}

// Matches: { id: "1", name: "John" }
// Matches: { id: "1", name: "John", email: "john@example.com" }
// Does NOT match: { id: 1, name: "John" } // id is number, not string
// Does NOT match: { id: "1" } // missing name

Type checking with is

Use is for inline type checking:

validate response {
assume .items is array,
assume .count is number,
assume .status is string
}

Combining schemas

Use schemas in complex match patterns:

schema PaginatedResponse {
data: array,
meta: {
page: number,
totalPages: number,
hasNext: boolean
}
}

schema EmptyResponse {
data: array,
meta: {
total: number
}
}

action FetchPaginated {
get "/items" { paginate: page(page, 100) }

match response {
PaginatedResponse where response.meta.hasNext == true -> continue,
PaginatedResponse -> store response.data -> items { key: .id },
EmptyResponse -> skip,
_ -> abort "Unexpected response"
}
}

Schema inheritance (via Vague)

Extend schemas using Vague's composition:

schema BaseEntity {
id: string,
createdAt: date,
updatedAt: date
}

schema User {
...BaseEntity,
name: string,
email: string
}

schema Order {
...BaseEntity,
customerId: string,
total: number
}

For advanced schema features, see the Vague documentation.

Best practices

Define schemas for API responses

mission APISync {
schema UserResponse {
users: array<User>,
pagination: {
page: number,
total: number
}
}

schema User {
id: string,
name: string,
email: string
}
}

Use schemas for error handling

schema APIError {
error: {
message: string,
code: string
}
}

schema AuthError {
error: {
message: string,
code: string
},
code: number // HTTP status code
}

action Fetch {
get "/data"

match response {
AuthError where .code == 401 -> jump RefreshToken then retry,
APIError -> abort response.error.message,
_ -> continue
}
}

Document expected formats

Schemas serve as documentation:

// XeroInvoice represents an invoice from Xero API
schema XeroInvoice {
InvoiceID: string,
InvoiceNumber: string,
Type: string, // ACCREC or ACCPAY
Contact: {
ContactID: string,
Name: string
},
LineItems: array<{
Description: string,
Quantity: number,
UnitAmount: number,
LineAmount: number
}>,
Total: number,
Status: string // DRAFT, SUBMITTED, AUTHORISED, PAID
}

Keep schemas close to usage

Define schemas in the same mission where they're used:

mission XeroSync {
// Schema definitions at the top
schema XeroInvoice { /* ... */ }
schema XeroContact { /* ... */ }

// Then sources, stores, actions...
}

Or use multi-file missions to organize:

missions/xero-sync/
├── mission.vague
├── schemas/
│ ├── invoice.vague
│ └── contact.vague
└── actions/
└── fetch.vague