Skip to content

Commit e778d93

Browse files
Feat: Codegen v3 experiment
1 parent 09919e7 commit e778d93

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+9749
-0
lines changed

experimental/README.md

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/pb33f/libopenapi"
11+
"gopkg.in/yaml.v3"
12+
13+
"github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen"
14+
)
15+
16+
func main() {
17+
configPath := flag.String("config", "", "path to configuration file")
18+
flagPackage := flag.String("package", "", "Go package name for generated code")
19+
flagOutput := flag.String("output", "", "output file path (default: <spec-basename>.gen.go)")
20+
flag.Usage = func() {
21+
fmt.Fprintf(os.Stderr, "Usage: %s [options] <spec-path>\n\n", os.Args[0])
22+
fmt.Fprintf(os.Stderr, "Arguments:\n")
23+
fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n")
24+
fmt.Fprintf(os.Stderr, "Options:\n")
25+
flag.PrintDefaults()
26+
}
27+
flag.Parse()
28+
29+
if flag.NArg() != 1 {
30+
flag.Usage()
31+
os.Exit(1)
32+
}
33+
34+
specPath := flag.Arg(0)
35+
36+
// Parse the OpenAPI spec
37+
specData, err := os.ReadFile(specPath)
38+
if err != nil {
39+
fmt.Fprintf(os.Stderr, "error reading spec: %v\n", err)
40+
os.Exit(1)
41+
}
42+
43+
doc, err := libopenapi.NewDocument(specData)
44+
if err != nil {
45+
fmt.Fprintf(os.Stderr, "error parsing spec: %v\n", err)
46+
os.Exit(1)
47+
}
48+
49+
// Parse config if provided, otherwise use empty config
50+
var cfg codegen.Configuration
51+
if *configPath != "" {
52+
configData, err := os.ReadFile(*configPath)
53+
if err != nil {
54+
fmt.Fprintf(os.Stderr, "error reading config: %v\n", err)
55+
os.Exit(1)
56+
}
57+
if err := yaml.Unmarshal(configData, &cfg); err != nil {
58+
fmt.Fprintf(os.Stderr, "error parsing config: %v\n", err)
59+
os.Exit(1)
60+
}
61+
}
62+
63+
// Flags override config file values
64+
if *flagPackage != "" {
65+
cfg.PackageName = *flagPackage
66+
}
67+
if *flagOutput != "" {
68+
cfg.Output = *flagOutput
69+
}
70+
71+
// Default output to <spec-basename>.gen.go
72+
if cfg.Output == "" {
73+
base := filepath.Base(specPath)
74+
ext := filepath.Ext(base)
75+
cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go"
76+
}
77+
78+
// Default package name from output file
79+
if cfg.PackageName == "" {
80+
cfg.PackageName = "api"
81+
}
82+
83+
// Generate code
84+
code, err := codegen.Generate(doc, cfg)
85+
if err != nil {
86+
fmt.Fprintf(os.Stderr, "error generating code: %v\n", err)
87+
os.Exit(1)
88+
}
89+
90+
// Write output
91+
if err := os.WriteFile(cfg.Output, []byte(code), 0644); err != nil {
92+
fmt.Fprintf(os.Stderr, "error writing output: %v\n", err)
93+
os.Exit(1)
94+
}
95+
96+
fmt.Printf("Generated %s\n", cfg.Output)
97+
}

experimental/go.mod

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module github.com/oapi-codegen/oapi-codegen/experimental
2+
3+
go 1.23.0
4+
5+
require (
6+
github.com/google/uuid v1.6.0
7+
github.com/pb33f/libopenapi v0.21.12
8+
github.com/stretchr/testify v1.10.0
9+
gopkg.in/yaml.v3 v3.0.1
10+
)
11+
12+
require (
13+
github.com/bahlo/generic-list-go v0.2.0 // indirect
14+
github.com/buger/jsonparser v1.1.1 // indirect
15+
github.com/davecgh/go-spew v1.1.1 // indirect
16+
github.com/mailru/easyjson v0.7.7 // indirect
17+
github.com/pmezard/go-difflib v1.0.0 // indirect
18+
github.com/speakeasy-api/jsonpath v0.6.2 // indirect
19+
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect
20+
)

experimental/go.sum

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
9+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
10+
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
11+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
12+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
13+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
14+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
15+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
16+
github.com/pb33f/libopenapi v0.21.12 h1:ityKYYWjiirJlz+slNaVF2NGfVF4Zn32H6CQEcrZQhg=
17+
github.com/pb33f/libopenapi v0.21.12/go.mod h1:utT5sD2/mnN7YK68FfZT5yEPbI1wwRBpSS4Hi0oOrBU=
18+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20+
github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ=
21+
github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
22+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
23+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
24+
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd h1:dLuIF2kX9c+KknGJUdJi1Il1SDiTSK158/BB9kdgAew=
25+
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo=
26+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
27+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
28+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
29+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
30+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)