Unions
Unions allow a value to be one of several types.
Simple Union
union :filter_value do
variant { string }
variant { integer }
endIntrospection:
{
"filter_value": {
"type": "union",
"variants": [
{
"type": "string"
},
{
"type": "integer"
}
]
}
}// TypeScript
export type FilterValue = number | string;
// Zod
export const FilterValueSchema = z.union([z.number().int(), z.string()]);Usage:
union :filter do
variant { string }
variant { integer }
endDiscriminated Union
A discriminated union uses a field to determine the variant:
union :filter, discriminator: :kind do
variant tag: 'string' do
object do
string :value
end
end
variant tag: 'range' do
object do
integer :gte
integer? :lte
end
end
endInput:
{ "kind": "string", "value": "hello" }
{ "kind": "range", "gte": 10, "lte": 20 }The discriminator field identifies which variant to use.
Introspection:
{
"filter": {
"type": "union",
"discriminator": "kind",
"variants": [
{
"tag": "string",
"type": "object",
"shape": {
"value": {
"type": "string"
}
}
},
{
"tag": "range",
"type": "object",
"shape": {
"gte": {
"type": "integer"
},
"lte": {
"type": "integer",
"optional": true
}
}
}
]
}
}// TypeScript
interface StringFilter {
kind: 'string';
value: string;
}
interface RangeFilter {
kind: 'range';
gte: number;
lte?: number;
}
type Filter = StringFilter | RangeFilter;
// Zod
export const FilterSchema = z.discriminatedUnion('kind', [
z.object({
kind: z.literal('string'),
value: z.string()
}),
z.object({
kind: z.literal('range'),
gte: z.number().int(),
lte: z.number().int().optional()
})
]);Variant Options
variant { string } # Primitive type
variant { reference :my_custom_type } # Reference to custom type
variant { array { string } } # Array type
variant { object { string :name } } # Inline object
variant tag: 'text' do # For discriminated unions
object { string :content }
end
variant partial: true do # Makes all fields optional
reference :my_type
endpartial
The partial: true option makes all fields in the variant optional:
union :user_update do
variant { reference :full_user }
variant tag: 'patch', partial: true do # All fields optional
reference :full_user
end
endRepresentation-Scoped Union
Unions can be defined on representations. They are copied to the contract that uses the representation:
class PostRepresentation < Apiwork::Representation::Base
union :content_block, discriminator: :kind do
variant tag: 'text' do
object do
string :body
end
end
variant tag: 'image' do
object do
string :url
integer :width
integer :height
end
end
end
attribute :content, type: :content_block
endThe attribute references the union by name. In exports, the union generates a dedicated type.
Contract-Scoped Union
class PostContract < Apiwork::Contract::Base
union :content_block do
variant do
object do
literal :type, value: 'text'
string :content
end
end
variant do
object do
literal :type, value: 'image'
string :url
end
end
end
endGenerated Output
OpenAPI 3.1
Simple unions use oneOf:
FilterValue:
oneOf:
- type: string
- type: integerDiscriminated unions add a discriminator object with propertyName and mapping:
Filter:
oneOf:
- $ref: '#/components/schemas/StringFilter'
- $ref: '#/components/schemas/RangeFilter'
discriminator:
propertyName: kind
mapping:
string: '#/components/schemas/StringFilter'
range: '#/components/schemas/RangeFilter'
StringFilter:
type: object
required: [kind, value]
properties:
kind:
type: string
enum: [string]
value:
type: string
RangeFilter:
type: object
required: [kind, gte]
properties:
kind:
type: string
enum: [range]
gte:
type: integer
lte:
type: integerThe discriminator.mapping tells OpenAPI clients exactly which schema to use based on the kind value. This enables proper validation and code generation in tools that support OpenAPI 3.1.
TypeScript
export type FilterValue = number | string;
export type Filter = StringFilter | RangeFilter;Zod
// Simple union
export const FilterValueSchema = z.union([z.number().int(), z.string()]);
// Discriminated union
export const FilterSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('string'), value: z.string() }),
z.object({ kind: z.literal('range'), gte: z.number().int(), lte: z.number().int().optional() })
]);See also
- Contract::Base reference —
unionmethod