Skip to content

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 combined
  • anyOf: Can match any of the specified schemas
  • oneOf: Must match exactly one of the specified schemas
  • if/then/else: Conditional schemas (OpenAPI 3.1) - uses runtime.Conditional[T, E]

The generator applies smart optimizations based on the union structure:

  1. Single element or nullable unions → Type bubbles up directly (no wrapper)
  2. Two-element unions → Uses runtime.Either[A, B] pattern
  3. Three+ element unions → Uses json.RawMessage with 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.

View the complete example


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.

View single element example

View nullable union example


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.

View the complete example


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

View the complete example


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.

View complex union example


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)
}

View basic example

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.

View then-only example

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]
}

View with-refs example