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, 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 combined
  • anyOf: Can match any of the specified schemas
  • oneOf: Must match exactly one of the specified schemas

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" 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.

View complex union example