Param Types
Primitive types are the building blocks. Every param uses one of these.
Scalar Types
| Type | Ruby | TypeScript | Zod |
|---|---|---|---|
:string | String | string | z.string() |
:integer | Integer | number | z.number().int() |
:number | Float | number | z.number() |
:decimal | BigDecimal | number | z.number() |
:boolean | true, false | boolean | z.boolean() |
:date | Date | string | z.iso.date() |
:datetime | DateTime, Time | string | z.iso.datetime() |
:time | Time | string | z.iso.time() |
:uuid | String (UUID format) | string | z.uuid() |
Special Types
| Type | Purpose | TypeScript | Zod |
|---|---|---|---|
:binary | Base64 encoded | string | z.string() |
:literal | Exact value | literal type | z.literal() |
:unknown | Any value | unknown | z.unknown() |
unknown vs object
| Type | Use when | TypeScript | Zod |
|---|---|---|---|
:unknown | Shape is arbitrary or unknown | unknown | z.unknown() |
:object | Shape is known and defined | { field: type } | z.object({...}) |
:unknown — Arbitrary data
JSONB columns auto-detect as :unknown. This is intentional:
# Database: metadata JSONB
attribute :metadata # type: :unknownGenerated TypeScript:
metadata: unknown;The client receives no type safety — they must validate at runtime.
:object — Explicit shape
Define the shape with a block:
attribute :metadata do
object do
string :version
array :tags do
string
end
end
endGenerated TypeScript:
metadata: {
version: string;
tags: string[];
};Now the client has full type safety.
When to use each:
| Scenario | Type | Reason |
|---|---|---|
| User-provided JSON (arbitrary) | :unknown | Shape varies |
| Settings/preferences | :object block | Known shape |
| Metadata with representation | :object block | Documented fields |
| Legacy flexible data | :unknown | Can't guarantee shape |
unknown vs array
The same principle applies to arrays. A JSONB column containing ["a", "b"] is still :unknown:
| Type | Use when | TypeScript | Zod |
|---|---|---|---|
:unknown | Array shape unknown | unknown | z.unknown() |
:array | Element type is known | Type[] | z.array(...) |
:unknown — Arbitrary data
# Database: tags JSONB (contains ["ruby", "rails"])
attribute :tags # type: :unknownGenerated TypeScript:
tags: unknown; // Not unknown[] — we don't even know it's an array:array — Explicit element type
attribute :tags do
array do
string
end
endGenerated TypeScript:
tags: string[];Array of objects:
attribute :line_items do
array do
object do
string :sku
integer :quantity
decimal :price
end
end
endGenerated TypeScript:
lineItems: {
sku: string;
quantity: number;
price: number;
}[];Array of unions:
attribute :notifications do
array do
union discriminator: :type do
variant tag: 'email' do
object do
string :address
end
end
variant tag: 'sms' do
object do
string :phone
end
end
end
end
endGenerated TypeScript:
notifications: (
| { type: 'email'; address: string }
| { type: 'sms'; phone: string }
)[];When to use each:
| Scenario | Type | Reason |
|---|---|---|
| Tags, labels, simple lists | array { string } | Known element type |
| Line items, addresses | array { object {...} } | Known shape |
| Mixed-type arrays with known variants | array { union {...} } | Known variant types |
| User-provided arrays | :unknown | Can't guarantee shape |
unknown vs record
Records represent Record<string, T> — objects with dynamic string keys and typed values. Use records when the keys are unknown but every value has the same type.
| Type | Use when | TypeScript | Zod |
|---|---|---|---|
:unknown | Shape is arbitrary | unknown | z.unknown() |
:record | Keys vary, values share a type | Record<string, T> | z.record(z.string(), T) |
:record — Typed values with dynamic keys
record :scores do
integer
endGenerated TypeScript:
scores: Record<string, number>;Record of objects:
record :settings do
object do
string :value
boolean :enabled
end
endGenerated TypeScript:
settings: Record<string, {
value: string;
enabled: boolean;
}>;When to use each:
| Scenario | Type | Reason |
|---|---|---|
| Scores, ratings, counters | record { integer } | Dynamic keys, typed values |
| Feature flags, settings | record { object {...} } | Dynamic keys, known value shape |
| Translations | record { string } | Locale keys, string values |
| Arbitrary JSON | :unknown | Can't guarantee value type |
Structure Types
| Type | Purpose |
|---|---|
:object | Nested object with shape |
:array | Array of items |
:record | Key-value map with typed values |
:union | One of several types |
Inline vs Named
Structure types can be defined inline or as named definitions:
# Inline object — defined in place
object :address do
string :street
end
# Named object — defined once, referenced by name
object :address do
string :street
end
reference :shipping, to: :addressObjects shows how to define reusable named objects. Unions covers discriminated and simple unions.
Semantic Precision
Some types produce the same runtime output but mean different things. This distinction is preserved in exports that support it.
| Type | Runtime | Purpose | OpenAPI |
|---|---|---|---|
:number | number | IEEE 754 floating-point | format: double |
:decimal | number | Arbitrary precision | — |
:binary | string | Base64-encoded bytes | format: byte |
When to use which:
:decimalfor money, percentages, and values where precision matters:numberfor scientific calculations, coordinates, and approximate values:binaryfor file uploads, images, and encoded payloads
The distinction matters even when the generated code is identical. OpenAPI consumers can use format hints for validation, documentation, and code generation. TypeScript and Zod treat these as equivalent at runtime.
Usage
string :title
integer :count
decimal :price
boolean :active
datetime :published_at
date :birth_date
uuid :idDate and Time Formats
Date and time types serialize to ISO 8601 strings:
| Type | Format | Example |
|---|---|---|
:date | YYYY-MM-DD | "2024-01-15" |
:datetime | YYYY-MM-DDTHH:MM:SSZ | "2024-01-15T10:30:00Z" |
:time | HH:MM:SS | "10:30:00" |
Literal Type
The :literal type constrains a value to an exact match. Used primarily in discriminated unions:
literal :type, value: 'text'// TypeScript
type: 'text'
// Zod
type: z.literal('text')Format Hints
The format option adds semantic metadata to exports. It does not change the Ruby type or validation — it provides hints for OpenAPI documentation and Zod validators.
string :email, format: :email
string :website, format: :url
integer :count, format: :int32| Type | Valid formats |
|---|---|
:string | :date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :text, :url, :uuid |
:integer | :int32, :int64 |
:decimal, :number | :double, :float |
Effect on exports:
| Export | Behavior |
|---|---|
| OpenAPI | Sets format on the schema property |
| Zod | Adds built-in validators (e.g., z.email(), z.uuid()) |
| TypeScript | No effect — types remain string, number |
Type Coercion
Query parameters and form data arrive as strings. Apiwork coerces them to their declared types before validation:
| Type | Input | Output |
|---|---|---|
:integer | "123" | 123 |
:number | "3.14" | 3.14 |
:boolean | "true", "1", "yes" | true |
:boolean | "false", "0", "no" | false |
:date | "2024-01-15" | Date |
:datetime | "2024-01-15T10:00:00Z" | Time |
:decimal | "99.99" | BigDecimal |
Generated Output
Introspection
{
"title": {
"type": "string"
},
"count": {
"type": "integer"
},
"price": {
"type": "decimal"
},
"active": {
"type": "boolean"
},
"published_at": {
"type": "datetime"
},
"birth_date": {
"type": "date"
},
"id": {
"type": "uuid"
}
}TypeScript
export interface Example {
title: string;
count: number;
price: number;
active: boolean;
publishedAt: string;
birthDate: string;
id: string;
}Zod
export const ExampleSchema = z.object({
title: z.string(),
count: z.number().int(),
price: z.number(),
active: z.boolean(),
publishedAt: z.iso.datetime(),
birthDate: z.iso.date(),
id: z.uuid(),
});OpenAPI
Example:
type: object
required:
- title
- count
- price
- active
- publishedAt
- birthDate
- id
properties:
title:
type: string
count:
type: integer
price:
type: number
active:
type: boolean
publishedAt:
type: string
format: date-time
birthDate:
type: string
format: date
id:
type: string
format: uuidSee also
- Contract::Object reference — all param options