Union Types¶
Union types in OpenAPI allow schemas to accept multiple different types or structures. The oapi-codegen generator handles allOf, anyOf, oneOf, and if/then/else with intelligent type generation based on the number and nature of the variants.
Overview¶
allOf: Merges all schemas into a single struct with all fields combinedanyOf: Can match any of the specified schemasoneOf: Must match exactly one of the specified schemasif/then/else: Conditional schemas (OpenAPI 3.1) - usesruntime.Conditional[T, E]
The generator applies smart optimizations based on the union structure:
- Single element or nullable unions → Type bubbles up directly (no wrapper)
- Two-element unions → Uses
runtime.Either[A, B]pattern - Three+ element unions → Uses
json.RawMessagewith accessor methods
allOf Merges All Schemas¶
When using allOf, all schemas are merged into a single struct containing all fields from all referenced schemas.
OpenAPI Spec¶
Order:
type: object
properties:
client:
allOf:
- $ref: '#/components/schemas/Identity'
- $ref: '#/components/schemas/Verification'
address:
allOf:
- type: string
Generated Go Code¶
type Order_Client struct {
Same *int `json:"same,omitempty"`
Issuer string `json:"issuer" validate:"required"`
Verifier *string `json:"verifier,omitempty"`
}
All fields from Identity (Issuer), Verification (Verifier), and the inline schema (Same) are merged into a single struct.
Bubble-Up for Single Element or Nullable Unions¶
When a union contains only one non-null element, the type simplifies directly to that element without creating a wrapper type.
Single Element Union¶
OpenAPI Spec:
Order:
type: object
properties:
client:
anyOf:
- $ref: '#/components/schemas/Identity'
Generated Go Code:
type Order struct {
Client *Identity `json:"client,omitempty"`
Verification *Verification `json:"verification,omitempty"`
Other map[string]string `json:"other,omitempty"`
}
No Order_Client_AnyOf wrapper is created—the type bubbles up to *Identity directly.
Nullable Union (anyOf/oneOf with null)¶
OpenAPI Spec:
name:
anyOf:
- type: string
- type: "null"
# oneOf with integer and null - should become *int
age:
oneOf:
- type: integer
- type: "null"
Generated Go Code:
type User struct {
Name *string `json:"name,omitempty"`
Age *int `json:"age,omitempty"`
Address *Address `json:"address,omitempty"`
Contact *Contact `json:"contact,omitempty"`
ID *string `json:"id,omitempty"`
}
Nullable unions become simple pointer types—no union wrapper types are created.
Two-Element Unions Use runtime.Either¶
When a union has exactly two non-null elements, the generator creates a type using runtime.Either[A, B].
OpenAPI Spec¶
Order:
type: object
properties:
client:
anyOf:
- $ref: '#/components/schemas/Identity'
- $ref: '#/components/schemas/Verification'
address:
anyOf:
- type: string
Generated Go Code¶
type Order_Client_AnyOf struct {
runtime.Either[Identity, Verification]
}
func (o *Order_Client_AnyOf) Validate() error {
if o.IsA() {
if v, ok := any(o.A).(runtime.Validator); ok {
return v.Validate()
}
}
if o.IsB() {
if v, ok := any(o.B).(runtime.Validator); ok {
return v.Validate()
}
}
return nil
}
The runtime.Either type provides IsA() and IsB() methods to check which variant is present.
Three+ Element Unions¶
For unions with three or more elements, the generator uses json.RawMessage storage with accessor methods for each type.
OpenAPI Spec (OpenAPI 3.1)¶
Measurement:
type: object
properties:
# Union of string or number
value:
type: ["string", "number"]
# Union of string or integer
count:
type: ["string", "integer", "boolean"]
# Union of boolean or string
flag:
type: ["boolean", "string"]
Generated Go Code¶
type Measurement_Count struct {
union json.RawMessage
}
Accessor methods for each type:
// Raw returns the union data inside the Measurement_Count as bytes
func (m *Measurement_Count) Raw() json.RawMessage {
return m.union
}
// AsString returns the union data inside the Measurement_Count as a string
func (m *Measurement_Count) AsString() (string, error) {
return runtime.UnmarshalAs[string](m.union)
}
// AsValidatedString returns the union data inside the Measurement_Count as a validated string
func (m *Measurement_Count) AsValidatedString() (string, error) {
val, err := m.AsString()
if err != nil {
var zero string
return zero, err
}
if err := m.validateString(val); err != nil {
var zero string
return zero, err
}
return val, nil
}
// FromString overwrites any union data inside the Measurement_Count as the provided string
func (m *Measurement_Count) FromString(val string) error {
// Validate before storing
if err := m.validateString(val); err != nil {
return err
}
bts, err := json.Marshal(val)
m.union = bts
return err
}
Each variant gets three methods:
- As*() - Retrieve the value as the specific type
- AsValidated*() - Retrieve and validate the value
- From*() - Set the value as the specific type
Complex Unions (allOf + anyOf + oneOf)¶
You can combine allOf with anyOf and oneOf in the same schema. The generator merges the allOf fields into the parent struct and injects the union type fields with json:"-" tags.
Union Field Injection¶
When a schema combines allOf with anyOf or oneOf, the union fields are injected into the parent struct with json:"-" tags to prevent direct JSON marshaling. The parent struct handles marshaling through custom MarshalJSON and UnmarshalJSON methods.
OpenAPI Spec¶
Order:
type: object
properties:
status:
$ref: '#/components/schemas/OrderStatus'
client:
allOf:
- description: should be ignored without type
- $ref: '#/components/schemas/Client'
- properties:
id:
type: integer
required:
- id
anyOf:
- $ref: '#/components/schemas/Identity'
- $ref: '#/components/schemas/Verification'
oneOf:
- $ref: '#/components/schemas/Address'
- $ref: '#/components/schemas/Location'
Generated Go Code¶
type Order_Client struct {
Name string `json:"name" validate:"required"`
ID int `json:"id"`
Order_Client_AnyOf *Order_Client_AnyOf `json:"-"`
Order_Client_OneOf *Order_Client_OneOf `json:"-"`
}
Notice the json:"-" tags on the union fields. These fields are not directly marshaled to JSON.
Custom Marshaling¶
The generator creates custom MarshalJSON and UnmarshalJSON methods that merge all parts together:
type _Alias_Order_Client Order_Client
baseJSON, err := json.Marshal((_Alias_Order_Client)(o))
if err != nil {
return nil, err
}
parts = append(parts, baseJSON)
{
b, err := runtime.MarshalJSON(o.Order_Client_AnyOf)
if err != nil {
return nil, fmt.Errorf("Order_Client_AnyOf marshal: %w", err)
}
parts = append(parts, b)
}
{
b, err := runtime.MarshalJSON(o.Order_Client_OneOf)
if err != nil {
return nil, fmt.Errorf("Order_Client_OneOf marshal: %w", err)
}
parts = append(parts, b)
}
return runtime.CoalesceOrMerge(parts...)
}
func (o *Order_Client) UnmarshalJSON(data []byte) error {
trim := bytes.TrimSpace(data)
The runtime.JSONMerge function combines all the JSON parts into a single object, ensuring that the base fields and union fields are properly merged.
Conditional Schemas (if/then/else)¶
OpenAPI 3.1 supports JSON Schema conditional keywords. The generator
treats then and else as structural branches. The if schema is
ignored - it's a validation predicate (e.g. "if kind equals typeA")
with no structural properties. if is also a reserved keyword in Go.
Both Branches - runtime.Conditional¶
When both then and else are present, the generator creates a
runtime.Conditional[T, E] wrapper with named variant types. Unlike
runtime.Either (.A/.B), Conditional uses .Then/.Else
fields and .IsThen()/.IsElse() methods.
OpenAPI Spec:
Resource:
type: object
properties:
kind:
type: string
if:
properties:
kind:
const: "typeA"
then:
type: object
properties:
fieldA:
type: string
valueA:
type: integer
else:
type: object
properties:
fieldB:
type: boolean
valueB:
type: number
Generated Go Code:
type Resource struct {
Kind *string `json:"kind,omitempty"`
Resource_IfThenElse *Resource_IfThenElse `json:"-"`
}
The variant types use _Then and _Else suffixes:
type Resource_Then struct {
FieldA *string `json:"fieldA,omitempty"`
ValueA *int `json:"valueA,omitempty"`
}
type Resource_Else struct {
FieldB *bool `json:"fieldB,omitempty"`
ValueB *float32 `json:"valueB,omitempty"`
}
type Resource_IfThenElse struct {
runtime.Conditional[Resource_Then, Resource_Else]
}
Usage:
if resource.Resource_IfThenElse.IsThen() {
fmt.Println(resource.Resource_IfThenElse.Then.FieldA)
}
if resource.Resource_IfThenElse.IsElse() {
fmt.Println(resource.Resource_IfThenElse.Else.FieldB)
}
Single Branch - Flat Merge¶
When only then or only else is present, the branch properties
are flat-merged into the parent struct. No wrapper type is created.
OpenAPI Spec:
Config:
type: object
properties:
enabled:
type: boolean
if:
properties:
enabled:
const: true
then:
type: object
properties:
timeout:
type: integer
retries:
type: integer
Generated Go Code:
type Config struct {
Enabled *bool `json:"enabled,omitempty"`
Timeout *int `json:"timeout,omitempty"`
Retries *int `json:"retries,omitempty"`
}
The timeout and retries fields from the then branch appear
directly on the Config struct.
With $ref Branches¶
When then/else reference component schemas, the generated
Conditional uses the referenced type names directly:
type Event_IfThenElse struct {
runtime.Conditional[ClickData, PageViewData]
}