Union Types¶
Union types in OpenAPI allow schemas to accept multiple different types or structures. The oapi-codegen generator handles allOf, anyOf, and oneOf 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 schemas
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" validate:"required"`
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:
func (o Order_Client) MarshalJSON() ([]byte, error) {
var parts []json.RawMessage
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...)
}
The runtime.JSONMerge function combines all the JSON parts into a single object, ensuring that the base fields and union fields are properly merged.