- Use Makefile settings or commands for building
- All commands located in
cmd/
- All
pkg/runtimefunctions should aim for 100% test coverage - Run
go test -cover ./pkg/runtime/...to check coverage
- Consult
examples/for sample configurations for different use cases - Running without config uses default settings:
go run ./cmd/oapi-codegen <spec-path>
output.use-single-file: true- Generate all code in one file (default) vs multiple filesoutput.directory- Output directory (whenuse-single-file: false, package name is appended as subdirectory)generate.client: true- Generate HTTP client codegenerate.models: false- Skip model generation (when models are in separate package)generate.validation.skip: true- Skip Validate() method generationgenerate.validation.response: true- Generate Validate() for response types (useful for contract testing)generate.always-prefix-enum-values: true- Prefix enum constants with type name (default)generate.default-int-type: int64- Use int64 instead of int for integer typesgenerate.handler.output.overwrite: true- Force regeneration of scaffold files (service.go, middleware.go)skip-prune: true- Keep unused types (normally pruned)error-mapping- Map response types to implement error interface (key: type name, value: json path to message)filter.include/exclude- Filter paths, tags, operation-ids, extensions
generate.handler.kind- Router framework:chi,echo,fiber,gin,std-http(required)generate.handler.name- Service interface name (default: "Service")generate.handler.models-package-alias- Prefix for model types when models are in separate packagegenerate.handler.validation.request/response- Enable request/response validation in handlersgenerate.handler.output.directory/package- Output for scaffold files (service.go, middleware.go)generate.handler.middleware: {}- Enable middleware.go generationgenerate.handler.server- Enable server/main.go generation withdirectory,port,timeout,handler-package
- After making changes to the code generator, ensure to run
make generatewhich regenerates the code for all OpenAPI specs inexamples/ - Verify that the generated code matches the expected output in
examples/ - Run
make testto ensure all unit tests pass - Consider running integration tests in Connexions to verify end-to-end functionality:
make test-integration. That could take 5 minutes.
- Re-run with
SPEC=<path-to-spec.yml> make test-integrationto focus on a specific spec - Never run all integration tests at once - always use SPEC= to limit scope
- Example:
make test-integration SPEC=3.0/github.com/ghes-3.5.1.1.4.yml - Never run oapi-codegen in the project root directory - always use
/tmpor run viamake test-integration
-
libopenapi limitation - JSON Pointer refs to paths
- Error:
component '#/paths/~1api~1v1~1...' does not exist in the specification - Cause: Spec uses
$refpointing to path elements which libopenapi can't resolve - Resolution: Skip spec by prefixing filename with
-(e.g.,mv spec.yml -spec.yml)
- Error:
-
Missing schema/component reference
- Error:
component '#/components/schemas/SomeName' does not exist - Cause: Spec references a schema that doesn't exist (broken spec)
- Resolution: Remove the spec from testdata
- Error:
-
x- prefixed schema names
- Error:
undefined: XAnyor similar - Cause: Spec has schemas named with
x-prefix (e.g.,x-any) which libopenapi treats as extensions - Resolution: Skip spec with
-prefix
- Error:
-
External file references
- Error:
unable to open the rolodex fileor../some-file.yaml does not exist - Cause: Spec references external files that aren't present
- Resolution: Remove the spec from testdata
- Error:
-
Build failures (undefined types, syntax errors)
- Check the generated code at the debug path shown in error output
- Look for patterns in the generated code that indicate the issue
- Fix the generator code, then re-run the specific spec
- Get the failing spec path from test output
- Run:
go run ./cmd/oapi-codegen <spec-path>to see generation errors - If generation succeeds, check the debug path for the generated code
- For libopenapi limitations, skip with
-prefix - For broken specs, remove from testdata
- For generator bugs, fix and re-test
When debugging complex issues, create a minimal reproducible example:
- Create a new directory in
examples/(e.g.,examples/issue-123/) - Add a simplified spec that reproduces the issue
- Add a
cfg.yamlwith the relevant configuration - Create a
gen_test.goto verify the generated code compiles:package example_test import ( "testing" _ "github.com/doordash-oss/oapi-codegen-dd/v3/examples/issue-123" ) func TestCompiles(t *testing.T) {}
- Run
go generate ./examples/issue-123/...to generate code - Run
go test ./examples/issue-123/...to verify it compiles - After fixing, ask the user whether to keep the example or remove it
testdata/specs- Specs being tested (if missing: runmake fetch-specsto download)- Never put generated files in project root - use
/tmpfor testing - Never build binaries in project root - use
go run ./cmd/oapi-codegeninstead ofgo build
The generator uses a two-pass approach for component schemas:
- Pass 1 (preRegisterSchemaNames): Registers all schema names and refs in TypeTracker BEFORE processing. This ensures cross-references can find the correct (potentially renamed) type names.
- Pass 2 (generateSchemaDefinitions): Generates full type definitions using pre-registered names.
This is critical for handling x-go-name extensions and name conflicts correctly.
Central registry for managing type names and references:
registerName(name)- Reserve a type nameregisterRef(ref, name)- Map a $ref to a type nameLookupByRef(ref)- Find type name for a $refLookupByName(name)- Find TypeDefinition by namegenerateUniqueName(name)- Generate unique name if conflict exists (appends numbers)
ParseOptions.visitedmap tracks visited schema paths to prevent infinite recursion- When a circular reference is detected, the generator returns a reference to the already-registered type
- Pre-registration in pass 1 ensures the type name exists before it's referenced
- Enums are generated for schemas with
enumvalues - Only comparable types can have enum constants (primitives like string, int, bool)
- Non-comparable types (time.Time, uuid.UUID, arrays, structs) skip enum constant generation
- Use
isComparableType()to check if a type can be used as an enum constant nonConstantTypesmap lists Go types that cannot be constants
- Response types can be configured to implement the
errorinterface viaoutput.response-type-suffixand error mapping - When a response type has error mapping, it cannot be an alias (aliases don't support methods)
- Response schemas are processed similarly to component schemas but with different SpecLocation
- Union types are generated as structs with pointer fields for each variant
- Use
ContainsUnions()method on schemas to check if they contain union elements - Union types get custom JSON marshaling/unmarshaling to handle the polymorphism
- When multiple schemas would generate the same Go type name, the generator appends numbers (e.g.,
Foo,Foo2,Foo3) - This happens at registration time via
generateUniqueName() - Conflicts can arise from: same names in different paths,
x-go-namecollisions, inline type extraction
When adding support for a new router/framework (e.g., mux), follow these steps:
In pkg/codegen/configuration.go:
- Add new constant:
HandlerKindMux HandlerKind = "mux"(keep constants in alphabetical order) - Update
IsValid()switch to include the new kind
In configuration-schema.json:
- Add the new kind to the
enumarray inHandlerOptions.kind(keep in alphabetical order)
Create directory pkg/codegen/templates/handler/<framework>/ with:
handler.tmpl- Required. Main handler template that includes shared templates via{{template "..."}}. See existing frameworks for pattern.server.tmpl- Required. Custom server setup using the framework's native server/middleware. Must wire up all 5 middleware types (recovery, request-id, logging, CORS, timeout) plus custom middleware from scaffold.middleware.tmpl- Optional. Override if framework needs custom middleware pattern (e.g., Echo has custom one)
Templates use {{define "handler-<framework>"}} blocks. The shared templates in pkg/codegen/templates/handler/ provide common functionality:
adapter.tmpl- Request/response adapterrouter.tmpl- Router registrationservice.tmpl- Service interface scaffoldservice-options.tmpl- Service request optionsresponse-data.tmpl- Response data typesmiddleware.tmpl- Default middleware scaffoldserver.tmpl- Default server main.go
Each framework's server.tmpl must demonstrate proper middleware setup:
- Recovery - Panic recovery middleware
- Request ID - Add unique request ID to each request
- Logging - Log request method, path, status
- CORS - Cross-origin resource sharing
- Timeout - Request timeout handling
- Custom middleware - Wire up
handler.ExampleMiddleware()when$config.Generate.Handler.Middlewareis set
Use the framework's native middleware where available. If the framework provides built-in middleware for 4+ of the above (e.g., Kratos has recovery, logging, tracing, metrics), you must still generate at least one example middleware in middleware.tmpl to demonstrate the pattern, and wire it up in server.tmpl. See echo/server.tmpl for reference.
Create examples/server/<framework>/ with:
cfg.yml- Config withgenerate.handler.kind: <framework>andgenerate.handler.output.overwrite: truegenerate.go-//go:generatedirectiveREADME.md- Documentation for the example (see below)- Generated files will be created in
api/andserver/subdirectories
Important: Standalone examples must have generate.handler.output.overwrite: true so scaffold files (service.go, middleware.go) are regenerated when the API spec changes.
Each server example must have a README.md with:
- Framework name and link to the framework's GitHub repository
- Description section with framework-specific notes (middleware pattern, path params, context type, etc.)
- Shell block showing how to start the server
- Separate curl examples for each of the 5 API endpoints:
GET /health- Health checkGET /users- List usersPOST /users- Create userGET /users/{id}- Get user by IDDELETE /users/{id}- Delete user
Create examples/server/test/<framework>/testcase/ with:
cfg.yml- Config for test casegenerate.go- Generate directive that copies shared service.go.src://go:generate cp ../../testcase/gen.go ./gen.go //go:generate cp ../../testcase/service.go.src ./service.go //go:generate go run github.com/doordash-oss/oapi-codegen-dd/v3/cmd/oapi-codegen -config cfg.yml ../../../api.yml
In examples/server/test/server_test.go:
- Add import for new testcase package
- Add new framework to the
frameworksslice in test functions
# Only regenerate the specific examples you're working on, NOT all examples
cd examples/server/<framework> && go generate ./...
cd examples/server/test/<framework> && go generate ./...
cd examples && go build ./... # Verify builds
cd examples && go test ./server/test/... # Run tests including new framework
make lint # Check for lint issuesImportant: Do NOT run make generate when adding a new framework - only regenerate the specific examples you're working on.
Documentation is in docs/ using MkDocs. Key files:
docs/mcp-server.md- MCP server generation guidedocs/server-generation.md- HTTP server generation guidedocs/extensions/x-*.md- Extension documentationmkdocs.yml- Navigation and config
- Create markdown file in
docs/ - Add to
nav:section inmkdocs.yml - For extensions, create in
docs/extensions/and add under Extensions nav
Documentation uses MkDocs snippets to include code from example files. The format is:
--8<-- "path/to/file.go:start_line:end_line"
For example: --8<-- "extensions/xgotype/gen.go:11:14" includes lines 11-14 from that file.
After regenerating examples, verify that line number references in docs are still correct:
grep -rn '\-\-8<\-\-' docs/ | grep -E ':[0-9]+:[0-9]+'Then check each referenced file to ensure the line ranges still point to the expected code.
pip install mkdocs-material
mkdocs serve