Migrating from oapi-codegen-dd
For projects on github.com/doordash-oss/oapi-codegen-dd/v3. A project still on v2 of the fork
follows the oapi-codegen guide instead.
The guide walks through examples/migration/oapi-codegen-dd: a petstore spec, the oapi-codegen-dd config, its translation, and the service and client calls after the move, which are built and tested.
What is different
Most of the move is renaming keys:
- The config has the same blocks under other keys.
- The service interface keeps its methods.
- The same extensions are read.
What changes in the code:
- There is a response data constructor per status:
NewGetPetResponseData(&pet)becomesNewGetPetResponseData200(&pet). - An error response is the error type, returned as the error. The adapter writes it with the
status the spec gives it, so
WithStatus(404)next to the error goes. - Param structs have other names.
GetPetPathbecomesGetPetPathParams, theHeaderfield becomesHeaders, and cookie params are read intoCookies. - The client returns
*Error, where it returnedError. Anerrors.Asneedsvar e *Error. GetPetWithResponsereturns aGetPetResponse, where it returned aGetPetResp. It returns no error for a status the spec documents.StatusCode()is a method.GetPetResponsewas the success body in oapi-codegen-dd. It is the envelope now.NewDefaultPetClient(baseURL)becomesNewPetClient(baseURL). There is noruntime.APIClient.- The MCP tools are built on the official Go SDK, and named in snake case.
- A union is a struct with one field per variant, whatever their number. There is no
runtime.Either. Validate()is plain generated code. Thevalidatestruct tags are gone.
Before and after
The handler
The handler of GET /pets/{id} in oapi-codegen-dd:
func (s *Service) GetPet(ctx context.Context, opts *GetPetServiceRequestOptions) (*GetPetResponseData, error) {
p, ok := s.pets.Get(opts.PathParams.ID)
if !ok {
return NewGetPetResponseData(nil).WithStatus(http.StatusNotFound), NewError("no such pet")
}
return NewGetPetResponseData(&p), nil
}
The same handler after the move, from service.go:
func (s *Service) GetPet(_ context.Context, opts *GetPetServiceRequestOptions) (*GetPetResponseData, error) {
p, ok := s.pets.Get(opts.PathParams.ID)
if !ok {
return nil, &Error{Code: http.StatusNotFound, Message: "no such pet"}
}
return NewGetPetResponseData200(&p), nil
}
return NewGetPetResponseData404(&Error{...}), nil gives the same response. NewRouter(svc)
mounts the service in both.
The client call
The client call in oapi-codegen-dd:
c, err := NewDefaultPetClient("http://localhost:8080")
if err != nil {
return err
}
pet, err := c.GetPet(ctx, &GetPetRequestOptions{PathParams: &GetPetPath{ID: 1}})
var notFound Error
if errors.As(err, ¬Found) {
return fmt.Errorf("no pet: %s", notFound.Message)
}
if err != nil {
return err
}
The same call after the move, as service_test.go makes it:
c, err := NewPetClient("http://localhost:8080")
if err != nil {
return err
}
pet, err := c.GetPet(ctx, &GetPetRequestOptions{PathParams: &GetPetPathParams{ID: 1}})
var notFound *Error
if errors.As(err, ¬Found) {
return fmt.Errorf("no pet: %s", notFound.Message)
}
if err != nil {
return err
}
The MCP server
The MCP server, before and after:
s := server.NewMCPServer("petstore", "1.0.0", server.WithToolCapabilities(true))
api.NewMCPTools(s, api.WithClient(client))
server.ServeStdio(s)
s := mcp.NewServer(&mcp.Implementation{Name: "petstore", Version: "1.0.0"}, nil)
api.NewMCPTools(client).Register(s)
s.Run(ctx, &mcp.StdioTransport{})
The tools are list_pets and get_pet now, where they were ListPets and GetPet.
- Prompts and host settings that name the old tool break.
x-mcp: {name: ListPets}on the operation keeps the old name (MCP). - A tool without a summary is described by its method and path,
GET /pets. It used to be described by its name.
Steps
- Add the CLI to the module:
go get -tool github.com/mockzilla/mockzilla-codegen/cmd/mockzilla-codegen. oapi-codegen-dd can stay until the build is green again. - Write
codegen.yamlnext to the spec, from the config table. An unknown key is an error that names its path, so a key that did not carry over shows on the first run. - Keep the spec. Rename two extensions:
x-oapi-codegen-extra-tagsisx-go-extra-tags, andx-oapi-codegen-only-honour-go-nameisx-go-name-exact. The old names are ignored, so keep both while both tools run. - Replace the
go:generateline with//go:generate go tool mockzilla-codegen generate -c codegen.yaml, and delete the old generated file. A service written from the old scaffold stays: it is your code. - Generate and run
go build ./.... Each error is a call site to move: constructors, param structs,*Error, envelopes. - Add
mockzilla-codegen generate -checkto CI, so a spec change without a new run fails there.
Reference
Command
| oapi-codegen-dd | mockzilla-codegen |
|---|---|
oapi-codegen -config cfg.yaml api.yaml |
mockzilla-codegen generate -c codegen.yaml, the spec path from spec.path or as the last argument |
go run github.com/doordash-oss/oapi-codegen-dd/v3/cmd/oapi-codegen ... |
go tool mockzilla-codegen generate ... (getting started) |
Config
| oapi-codegen-dd | mockzilla-codegen |
|---|---|
package |
package |
copyright-header |
header |
skip-prune |
spec.prune: false |
overlay.sources |
spec.overlays |
output.use-single-file: true, output.directory, output.filename |
output.file, the path of that file |
output.use-single-file: false |
output.files, each part to the file you name (output files) |
output.skip-fmt |
output.format: false |
generate.models |
always on; models for another package go there with output.files |
generate.client |
client: |
generate.client-with-response |
client.with-response |
generate.client-streaming |
client.streaming |
generate.omit-description |
models.descriptions: false |
generate.default-int-type |
models.int-type |
generate.always-prefix-enum-values |
naming.enum-prefix |
generate.additional-tags |
models.extra-tags |
generate.validation.skip, response |
models.validation.skip, response |
generate.handler.kind |
server.framework, same names |
generate.handler.name |
server.name |
generate.handler.multipart-max-memory: 64 |
server.multipart-max-memory: 64MB, with a unit |
generate.handler.validation.request, response |
server.validation.request, response |
generate.handler.service: {} |
server.scaffold.service: ./service.go |
generate.handler.middleware: {} |
server.scaffold.middleware: ./middleware.go |
generate.handler.output.directory, package |
the folder of the scaffold paths; its package from output.packages |
generate.handler.output.overwrite |
server.scaffold.overwrite |
generate.handler.server.directory |
server.scaffold.main: ./server/main.go |
generate.handler.server.port, timeout: 30 |
server.scaffold.port, timeout: 30s |
generate.mcp-server |
mcp: |
generate.mcp-server.default-skip |
mcp.default-skip |
filter.include, filter.exclude and their keys |
spec.filter.include, spec.filter.exclude, unchanged |
additional-imports |
imports; no . alias |
error-mapping |
models.error-mapping |
client.name, client.timeout |
client.name, client.timeout |
user-templates |
templates for the blocks that may be replaced, else extra-files |
user-context |
user-context |
The keys of error-mapping are type names in both. An error response written inline in an
operation is <Op>Response<Status> here (names),
where it was <Op>ErrorResponse.
Not carried over
base-path: a relative$refis resolved against the spec file.generate.validation.simple: validation is plain generated code (validation).generate.handler.server.handler-package: the module path comes fromgo.mod, oroutput.module.generate.handler.models-package,handler-package-alias,models-package-alias: one run writes every folder and the imports between them.
Extensions
The list is the same, x-mcp included (extensions), apart from these:
| oapi-codegen-dd | mockzilla-codegen |
|---|---|
x-oapi-codegen-extra-tags |
x-go-extra-tags; the old name is ignored |
x-oapi-codegen-only-honour-go-name |
x-go-name-exact; the old name is ignored |
x-go-type-name on a component |
declares the type under the new name only, without an alias under the component name |
Server
| oapi-codegen-dd | mockzilla-codegen |
|---|---|
<Name>Interface, methods (ctx, *<Op>ServiceRequestOptions) (*<Op>ResponseData, error) |
the same (server) |
New<Op>ResponseData(body), .WithStatus(code) |
New<Op>ResponseData<Status>(body) per status; WithStatus stays (response data) |
(resp.WithStatus(404), err) for an error response |
(nil, err) with the mapped error type, written with the status of the first response that carries it (HTTP adapter) |
<Op>Path, opts.Header |
<Op>PathParams, opts.Headers; opts.Cookies too |
NewRouter(svc, opts...) |
the same, with WithMiddleware, WithErrorHandler and WithRouter (router) |
NewHTTPAdapter(svc, errHandler) |
NewHTTPAdapter(svc, opts...), the error handler through WithErrorHandler |
| scaffolds in a folder next to the output | where server.scaffold says; main imports the others by module path |
Client
| oapi-codegen-dd | mockzilla-codegen |
|---|---|
NewDefaultPetClient(baseURL, opts...), NewPetClient(runtime.APIClient) |
NewPetClient(baseURL, opts...); WithHTTPClient takes anything with Do (client) |
<Op>(ctx, options, reqEditors...) returning the success body |
the same, editors after those of WithRequestEditor on the client (methods) |
an error that unwraps to Error |
an error that unwraps to *Error |
<Op>WithResponse returning <Op>Resp and an error for a 4xx |
<Op>WithResponse returning <Op>Response with HTTPResponse, Body, JSON<status> and Headers<status>, and no error for a documented status (envelopes) |
<Op>Response, the success body |
the envelope; the body is its own type, Pet here |
<Op>Stream over runtime.Stream[T] |
the same (streaming) |
Types
- A
oneOforanyOfis a struct with one field per variant, whatever their number (unions). Before, it wasruntime.Either[A, B]for two variants and a raw message with accessors for more. - A
numberwithout a format isfloat64(type mapping). - The pointer rules and naming rules are written down. Compare a generated file against the old one before touching call sites.