Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on the real value-add for your organization.
- **Smart pruning** - Automatically removes unused types by default (configurable)

### Type System
- **Union types** - Full support for `oneOf`, `anyOf`, and `allOf` with intelligent type merging and `runtime.Either[A, B]` for two-element unions
- **Union types** - Full support for `oneOf`, `anyOf`, `allOf`, and `if`/`then`/`else` with intelligent type merging, `runtime.Either[A, B]` for two-element unions, and `runtime.Conditional[T, E]` for conditional schemas
- **Additional properties** - Handle dynamic fields with `map[string]interface{}` or custom types
- **Validation** - Built-in validation using [go-playground/validator](https://github.com/go-playground/validator) with `Validate()` methods on generated types
- **Custom extensions** - for fine-grained control over code generation
Expand Down
83 changes: 82 additions & 1 deletion docs/union-types.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# 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.
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:

Expand Down Expand Up @@ -167,3 +168,83 @@ The `runtime.JSONMerge` function combines all the JSON parts into a single objec

[View complex union example](https://github.com/doordash-oss/oapi-codegen-dd/tree/main/examples/union/allof-anyof-oneof/){:target="_blank"}

---

## 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:**

```yaml
--8<-- "ifthenelse/basic/api.yaml:20:42"
```

**Generated Go Code:**

```go
--8<-- "ifthenelse/basic/gen.go:16:19"
```

The variant types use `_Then` and `_Else` suffixes:

```go
--8<-- "ifthenelse/basic/gen.go:86:98"
```

Usage:

```go
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](https://github.com/doordash-oss/oapi-codegen-dd/tree/main/examples/ifthenelse/basic/){:target="_blank"}

### 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:**

```yaml
--8<-- "ifthenelse/then-only/api.yaml:20:35"
```

**Generated Go Code:**

```go
--8<-- "ifthenelse/then-only/gen.go:12:16"
```

The `timeout` and `retries` fields from the `then` branch appear
directly on the `Config` struct.

[View then-only example](https://github.com/doordash-oss/oapi-codegen-dd/tree/main/examples/ifthenelse/then-only/){:target="_blank"}

### With `$ref` Branches

When `then`/`else` reference component schemas, the generated
`Conditional` uses the referenced type names directly:

```go
--8<-- "ifthenelse/with-refs/gen.go:98:100"
```

[View with-refs example](https://github.com/doordash-oss/oapi-codegen-dd/tree/main/examples/ifthenelse/with-refs/){:target="_blank"}

42 changes: 42 additions & 0 deletions examples/ifthenelse/basic/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
openapi: "3.1.0"
info:
version: 1.0.0
title: Basic if/then/else
description: Both then and else branches produce a union (Either) type
paths:
/resources:
get:
operationId: listResources
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Resource'

components:
schemas:
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
3 changes: 3 additions & 0 deletions examples/ifthenelse/basic/cfg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# yaml-language-server: $schema=../../configuration-schema.json
package: gen
skip-prune: true
119 changes: 119 additions & 0 deletions examples/ifthenelse/basic/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions examples/ifthenelse/basic/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package gen

//go:generate go run github.com/doordash-oss/oapi-codegen-dd/v3/cmd/oapi-codegen -config cfg.yaml api.yaml
48 changes: 48 additions & 0 deletions examples/ifthenelse/inside-allof/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
openapi: "3.1.0"
info:
version: 1.0.0
title: if/then/else inside allOf
description: Conditional branches nested inside an allOf composition
paths:
/resources:
get:
operationId: listResources
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SpecialResource'

components:
schemas:
BaseResource:
type: object
properties:
id:
type: string
name:
type: string

SpecialResource:
allOf:
- $ref: '#/components/schemas/BaseResource'
- type: object
properties:
category:
type: string
if:
properties:
category:
const: "premium"
then:
type: object
properties:
premiumFeature:
type: string
else:
type: object
properties:
standardLimit:
type: integer
3 changes: 3 additions & 0 deletions examples/ifthenelse/inside-allof/cfg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# yaml-language-server: $schema=../../configuration-schema.json
package: gen
skip-prune: true
Loading
Loading