|
| 1 | +# oapi-codegen experimental |
| 2 | + |
| 3 | +Ground-up rewrite of oapi-codegen with: |
| 4 | + |
| 5 | +- **OpenAPI 3.1/3.2 support** via [libopenapi](https://github.com/pb33f/libopenapi) (replacing kin-openapi) |
| 6 | +- **Clean separation**: model generation as core, routers as separate layers |
| 7 | +- **No runtime package** - generated code is self-contained |
| 8 | +- **Proper anyOf/oneOf union types** with correct marshal/unmarshal semantics |
| 9 | +- **allOf flattening** - properties merged into single struct, not embedding |
| 10 | +- **Default values** - generated `ApplyDefaults()` methods |
| 11 | + |
| 12 | +## Usage |
| 13 | + |
| 14 | +```bash |
| 15 | +go run ./cmd/oapi-codegen -package myapi -output types.gen.go openapi.yaml |
| 16 | +``` |
| 17 | + |
| 18 | +Options: |
| 19 | +- `-package <name>` - Go package name for generated code |
| 20 | +- `-output <file>` - Output file path (default: `<spec-basename>.gen.go`) |
| 21 | +- `-config <file>` - Path to configuration file |
| 22 | + |
| 23 | +## Structure |
| 24 | + |
| 25 | +``` |
| 26 | +experimental/ |
| 27 | +├── cmd/oapi-codegen/ # CLI tool |
| 28 | +├── internal/codegen/ |
| 29 | +│ ├── codegen.go # Main generation orchestration |
| 30 | +│ ├── gather.go # Schema discovery from OpenAPI doc |
| 31 | +│ ├── schema.go # SchemaDescriptor, SchemaPath types |
| 32 | +│ ├── typegen.go # Go type expression generation |
| 33 | +│ ├── output.go # Code construction utilities |
| 34 | +│ ├── namemangling.go # Go identifier conversion |
| 35 | +│ ├── typemapping.go # OpenAPI → Go type mappings |
| 36 | +│ ├── templates/ # Custom type templates |
| 37 | +│ │ └── types/ # Email, UUID, File types |
| 38 | +│ └── test/ # Test suites |
| 39 | +│ ├── files/ # Test OpenAPI specs |
| 40 | +│ ├── comprehensive/ # Full feature tests |
| 41 | +│ ├── default_values/ # ApplyDefaults tests |
| 42 | +│ └── nested_aggregate/ # Union nesting tests |
| 43 | +``` |
| 44 | + |
| 45 | +## Features |
| 46 | + |
| 47 | +### Type Generation |
| 48 | + |
| 49 | +| OpenAPI | Go | |
| 50 | +|---------|-----| |
| 51 | +| `type: object` with properties | `struct` | |
| 52 | +| `type: object` with only `additionalProperties` | `map[string]T` | |
| 53 | +| `type: object` with both | `struct` with `AdditionalProperties` field | |
| 54 | +| `enum` | `type T string` with `const` block | |
| 55 | +| `allOf` | Flattened `struct` (properties merged) | |
| 56 | +| `anyOf` | Union `struct` with pointer fields + marshal/unmarshal | |
| 57 | +| `oneOf` | Union `struct` with exactly-one enforcement | |
| 58 | +| `$ref` | Resolved to target type name | |
| 59 | + |
| 60 | +### Pointer Semantics |
| 61 | + |
| 62 | +- **Required + non-nullable** → value type |
| 63 | +- **Optional or nullable** → pointer type |
| 64 | +- **Slices and maps** → never pointers (nil is zero value) |
| 65 | + |
| 66 | +### Default Values |
| 67 | + |
| 68 | +All structs get an `ApplyDefaults()` method: |
| 69 | + |
| 70 | +```go |
| 71 | +type Config struct { |
| 72 | + Name *string `json:"name,omitempty"` |
| 73 | + Timeout *int `json:"timeout,omitempty"` |
| 74 | +} |
| 75 | + |
| 76 | +func (s *Config) ApplyDefaults() { |
| 77 | + if s.Name == nil { |
| 78 | + v := "default-name" |
| 79 | + s.Name = &v |
| 80 | + } |
| 81 | + if s.Timeout == nil { |
| 82 | + v := 30 |
| 83 | + s.Timeout = &v |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +Usage pattern: |
| 89 | +```go |
| 90 | +var cfg Config |
| 91 | +json.Unmarshal(data, &cfg) |
| 92 | +cfg.ApplyDefaults() |
| 93 | +``` |
| 94 | + |
| 95 | +### Union Types (anyOf/oneOf) |
| 96 | + |
| 97 | +```go |
| 98 | +// Generated for oneOf with two object variants |
| 99 | +type MyUnion struct { |
| 100 | + VariantA *TypeA |
| 101 | + VariantB *TypeB |
| 102 | +} |
| 103 | + |
| 104 | +// MarshalJSON enforces exactly one variant set |
| 105 | +// UnmarshalJSON tries each variant, expects exactly one match |
| 106 | +``` |
| 107 | + |
| 108 | +## Configuration |
| 109 | + |
| 110 | +Create a YAML configuration file: |
| 111 | + |
| 112 | +```yaml |
| 113 | +# config.yaml |
| 114 | +package: myapi |
| 115 | +output: types.gen.go |
| 116 | + |
| 117 | +# Type mappings: OpenAPI type/format → Go type |
| 118 | +type-mapping: |
| 119 | + integer: |
| 120 | + default: |
| 121 | + type: int |
| 122 | + formats: |
| 123 | + int64: |
| 124 | + type: int64 |
| 125 | + number: |
| 126 | + default: |
| 127 | + type: float64 # Override default from float32 |
| 128 | + string: |
| 129 | + formats: |
| 130 | + date-time: |
| 131 | + type: time.Time |
| 132 | + import: time |
| 133 | + uuid: |
| 134 | + type: uuid.UUID |
| 135 | + import: github.com/google/uuid |
| 136 | + # Custom format mapping |
| 137 | + money: |
| 138 | + type: decimal.Decimal |
| 139 | + import: github.com/shopspring/decimal |
| 140 | + |
| 141 | +# Name mangling: controls how OpenAPI names become Go identifiers |
| 142 | +name-mangling: |
| 143 | + # Prefix for names starting with digits |
| 144 | + numeric-prefix: "N" # default: "N" |
| 145 | + # Prefix for Go reserved keywords |
| 146 | + reserved-prefix: "" # default: "" (uses suffix instead) |
| 147 | + # Suffix for Go reserved keywords |
| 148 | + reserved-suffix: "_" # default: "_" |
| 149 | + # Known initialisms (keep uppercase) |
| 150 | + initialisms: |
| 151 | + - ID |
| 152 | + - HTTP |
| 153 | + - URL |
| 154 | + - API |
| 155 | + - JSON |
| 156 | + - XML |
| 157 | + - UUID |
| 158 | + # Character substitutions |
| 159 | + character-substitutions: |
| 160 | + "+": "Plus" |
| 161 | + "@": "At" |
| 162 | + |
| 163 | +# Name substitutions: direct overrides for specific names |
| 164 | +name-substitutions: |
| 165 | + # Override individual schema/property names during conversion |
| 166 | + type-names: |
| 167 | + foo: MyCustomFoo # "foo" becomes "MyCustomFoo" instead of "Foo" |
| 168 | + property-names: |
| 169 | + bar: MyCustomBar # "bar" field becomes "MyCustomBar" |
| 170 | +``` |
| 171 | +
|
| 172 | +Use with `-config`: |
| 173 | +```bash |
| 174 | +go run ./cmd/oapi-codegen -config config.yaml openapi.yaml |
| 175 | +``` |
| 176 | + |
| 177 | +### Default Type Mappings |
| 178 | + |
| 179 | +| OpenAPI Type | Format | Go Type | |
| 180 | +|--------------|--------|---------| |
| 181 | +| `integer` | (none) | `int` | |
| 182 | +| `integer` | `int32` | `int32` | |
| 183 | +| `integer` | `int64` | `int64` | |
| 184 | +| `number` | (none) | `float32` | |
| 185 | +| `number` | `double` | `float64` | |
| 186 | +| `boolean` | (none) | `bool` | |
| 187 | +| `string` | (none) | `string` | |
| 188 | +| `string` | `byte` | `[]byte` | |
| 189 | +| `string` | `date-time` | `time.Time` | |
| 190 | +| `string` | `uuid` | `UUID` (custom type) | |
| 191 | +| `string` | `email` | `Email` (custom type) | |
| 192 | +| `string` | `binary` | `File` (custom type) | |
| 193 | + |
| 194 | +### Name Override Limitations |
| 195 | + |
| 196 | +> **Note:** The current `name-substitutions` system only overrides individual name *parts* during conversion, not full generated type names. |
| 197 | +> |
| 198 | +> For example, if you have a schema at `#/components/schemas/Cat`: |
| 199 | +> - Setting `type-names: {Cat: Kitty}` will produce `KittySchemaComponent` (stable) and `Kitty` (short) |
| 200 | +> - You cannot currently override the full stable name `CatSchemaComponent` to something completely different |
| 201 | +> |
| 202 | +> Full type name overrides (by schema path or generated name) are not yet implemented. |
| 203 | + |
| 204 | +## Development |
| 205 | + |
| 206 | +Run tests: |
| 207 | +```bash |
| 208 | +go test ./internal/codegen/... |
| 209 | +``` |
| 210 | + |
| 211 | +Regenerate test outputs: |
| 212 | +```bash |
| 213 | +go run ./cmd/oapi-codegen -package output -output internal/codegen/test/comprehensive/output/comprehensive.gen.go internal/codegen/test/files/comprehensive.yaml |
| 214 | +``` |
| 215 | + |
| 216 | +## Status |
| 217 | + |
| 218 | +**Active development.** Model generation is working for most schema types. |
| 219 | + |
| 220 | +Working: |
| 221 | +- [x] Object schemas → structs |
| 222 | +- [x] Enum schemas → const blocks |
| 223 | +- [x] allOf → flattened structs |
| 224 | +- [x] anyOf/oneOf → union types |
| 225 | +- [x] additionalProperties |
| 226 | +- [x] $ref resolution |
| 227 | +- [x] Nested/inline schemas |
| 228 | +- [x] Default values (`ApplyDefaults()`) |
| 229 | +- [x] Custom format types (email, uuid, binary) |
| 230 | + |
| 231 | +Not yet implemented: |
| 232 | +- [ ] Request/response body generation |
| 233 | +- [ ] Operation/handler generation |
| 234 | +- [ ] Router integrations |
| 235 | +- [ ] Validation |
| 236 | +- [ ] Client generation |
| 237 | + |
| 238 | +See [CONTEXT.md](CONTEXT.md) for detailed design decisions and architecture notes. |
0 commit comments