Migrating from oapi-codegen
For projects on github.com/oapi-codegen/oapi-codegen/v2, or the older deepmap/oapi-codegen
import path.
The guide walks through examples/migration/oapi-codegen: a petstore spec, the oapi-codegen config, its translation, and the service and client calls after the move, which are built and tested.
What is different
- There is one server shape: a method of the service interface. It gets
ctxand one options struct, and returns response data or an error. There is nohttp.ResponseWriter. A strict server handler keeps its flow. A plainServerInterfacehandler is rewritten. - Error schemas are Go errors. With
models.error-mapping,Errorgets anError()method. The service returns it, and it is written with the status the spec gives it. - The client returns the success body.
GetPetreturns*Pet, and any other status is an error.client.with-response: trueadds the<Op>WithResponsemethods, on the same client. - The params of an operation sit in one options struct, with a field for each place they go:
PathParams,Query,Headers,CookiesandBody. IdbecomesID. The naming rules are fixed, and close toToCamelCaseWithInitialisms.- A union is a struct with one field per variant, not a raw message with
AsandFrommethods. - Every type gets
Validate() error. The server can check requests and responses with plain generated code, without kin-openapi (validation).
Before and after
The handler
The handler of GET /pets/{id} in the strict server, and how it is mounted:
func (s *Server) GetPet(ctx context.Context, req GetPetRequestObject) (GetPetResponseObject, error) {
p, ok := s.pets.Get(req.Id)
if !ok {
return GetPet404JSONResponse{Code: http.StatusNotFound, Message: "no such pet"}, nil
}
return GetPet200JSONResponse(p), nil
}
h := HandlerFromMux(NewStrictHandler(&Server{}, nil), chi.NewRouter())
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
}
h := NewRouter(&Service{})
return NewGetPetResponseData404(&Error{...}), nil gives the same response. Returning the error
lets code deep in the service fail with the type the spec documents.
The client call
The client call, with ClientWithResponses:
c, err := NewClientWithResponses("http://localhost:8080")
if err != nil {
return err
}
rsp, err := c.GetPetWithResponse(ctx, 1)
if err != nil {
return err
}
if rsp.JSON404 != nil {
return fmt.Errorf("no pet: %s", rsp.JSON404.Message)
}
pet := rsp.JSON200
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
}
GetPetWithResponse is still there: it returns the envelope with JSON404 set and no error.
Steps
- Add the CLI to the module:
go get -tool github.com/mockzilla/mockzilla-codegen/cmd/mockzilla-codegen. oapi-codegen 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. Delete the old generated file, or pointoutput.fileat it. - Generate and run
go build ./.... Each error is a call site to move: handlers as above, client calls,IdtoID, unions. - Add
mockzilla-codegen generate -checkto CI, so a spec change without a new run fails there.
Reference
Command
| oapi-codegen | 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:generate go tool oapi-codegen -config cfg.yaml ../api.yaml |
//go:generate go tool mockzilla-codegen generate -c ../codegen.yaml |
oapi-codegen -version |
mockzilla-codegen version |
-package api -o api.gen.go |
-package api -o api.gen.go |
-generate types,client,chi-server |
-client -server chi; models are always written |
| the other command line flags | config keys |
Config
| oapi-codegen | mockzilla-codegen |
|---|---|
package |
package |
output |
output.file |
generate.models |
always on |
generate.chi-server, echo-server, echo5-server, gin-server, gorilla-server, iris-server, std-http-server, fiber-v3-server |
server.framework: chi, echo, echo-v5, gin, gorilla-mux, iris, std-http, fiber |
generate.strict-server |
always: the service interface is the only server shape |
generate.client |
client: |
output-options.client-type-name |
client.name |
output-options.skip-fmt |
output.format: false |
output-options.skip-prune |
spec.prune: false |
output-options.include-tags, exclude-tags |
spec.filter.include.tags, spec.filter.exclude.tags |
output-options.include-operation-ids, exclude-operation-ids |
spec.filter.include.operation-ids, spec.filter.exclude.operation-ids |
output-options.exclude-schemas |
spec.filter.exclude.schema-properties drops properties; an overlay removes a whole schema |
output-options.overlay.path |
spec.overlays: [path], several in order |
output-options.additional-initialisms |
naming.initialisms, which applies without a name-normalizer |
output-options.yaml-tags, struct-tags |
models.extra-tags: [yaml]; each tag repeats the JSON name |
output-options.skip-enum-validate |
models.validation.skip, for every Validate method |
output-options.user-templates |
templates for the blocks that may be replaced, else extra-files |
output-options.prefer-skip-optional-pointer-on-container-types |
the default: a slice or map never gets a pointer |
output-options.nullable-type |
models.nullable: true: every field that may be absent or null is a runtime.Nullable[T], optional ones too (nullable) |
output-options.streaming-content-types |
client.streaming reads text/event-stream and line-delimited JSON; the list is fixed |
output-options.resolve-type-name-collisions |
always (naming) |
output-options.generate-types-for-anonymous-schemas |
always: every inline object is a named type |
compatibility.always-prefix-enum-values |
naming.enum-prefix, on by default |
compatibility.allow-unexported-struct-field-names |
x-go-name-exact per field |
compatibility.apply-chi-middleware-first-to-last, apply-gorilla-middleware-first-to-last |
the default: WithMiddleware wraps outermost first |
compatibility.disable-required-readonly-as-pointer |
the default: a required readOnly field is a plain value with omitempty |
additional-imports |
imports, same package and alias; no . alias |
models.error-mapping has no oapi-codegen key. It turns an error schema into a Go error, as in
Before and after.
Not carried over
generate.fiber-server:fiberis Fiber v3; Fiber v2 has no router here.generate.embedded-specandGetSwagger(): embed the spec yourself with//go:embed.generate.server-urls: the client takes a base URL string.output-options.name-normalizer: the naming rules are fixed. They are closest toToCamelCaseWithInitialisms, so a project onToCamelCaseseesIdbecomeID.output-options.prefer-skip-optional-pointer: there is no global switch. Usex-go-type-skip-optional-pointeron a field.output-options.response-type-suffix,content-types: type names follow fixed rules,<Op>Response<Status>and<Op>JSONRequestBody(names).output-options.type-mapping,disable-type-aliases-for-type: usex-go-typeon a schema, andmodels.int-typefor integers without a format.output-options.client-response-bytes-function,skip-client-response-content-type,skip-response-body-getters: an envelope always hasBodyandHTTPResponse, and a field per documented body.output-options.lenient-union-accessors,skip-enum-via-oneof: unions and enums have one shape (unions).output-options.prefer-skip-optional-pointer-with-omitzero:omitzerois added where a struct hasomitempty. An optional slice or map hasomitzeroalone, so an empty one is sent.compatibility.schema-merging-behavior,old-merge-schemas,old-allof-sibling-merging,old-enum-conflicts,old-aliasing: anallOfis merged into one type, with its sibling properties (allOf), and enum constants are prefixed. The old behaviours cannot be brought back.compatibility.disable-flatten-additional-properties: an object without properties is a map.compatibility.headers-implicitly-required: response headers follow theirrequiredflag.compatibility.sort-handler-registrations: routes are registered in spec order.compatibility.enable-auth-scopes-on-context,circular-reference-limit,preserve-original-operation-id-casing-in-embedded-spec: nothing to replace.import-mapping: a$refinto another file is resolved, and its types are generated with the rest. To keep several packages, run once withoutput.filesandoutput.packages, which send parts to their folders (output files). The imports between them are written for you.
Extensions
Every extension oapi-codegen documents keeps its meaning, apart from these (extensions):
| oapi-codegen | 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-enum-varnames, x-enumNames |
x-enum-names |
x-omitzero |
none; omitzero is written where a struct, slice or map needs it |
x-order |
none; fields keep the order of the spec |
x-oapi-codegen-enum-merge |
none |
x-go-type-name on a component |
declares the type under the new name only, no alias under the component name |
Server
| oapi-codegen | mockzilla-codegen |
|---|---|
ServerInterface, methods (w http.ResponseWriter, r *http.Request, params P) |
ServiceInterface, methods (ctx, *<Op>ServiceRequestOptions) (*<Op>ResponseData, error) |
StrictServerInterface, methods (ctx, <Op>RequestObject) (<Op>ResponseObject, error) |
the same interface; <Op>RequestObject.Params.Limit is opts.Query.Limit, a path param req.Id is opts.PathParams.ID, .Body is opts.Body |
<Op>200JSONResponse{...} returned as the response object |
New<Op>ResponseData200(&body), headers with .WithTypedHeaders(...) (response data) |
NewStrictHandler(ssi, middlewares) |
nothing; the adapter is the strict handler |
Handler(si), HandlerFromMux(si, r), HandlerWithOptions(si, opts) |
NewRouter(svc, opts...); WithRouter(r) registers on an existing router |
ServerInterfaceWrapper |
HTTPAdapter, NewHTTPAdapter(svc, opts...) for hand-written routing |
ChiServerOptions.Middlewares, MiddlewareFunc |
WithMiddleware(func(http.Handler) http.Handler) |
ErrorHandlerFunc, RequestErrorHandlerFunc |
WithErrorHandler(ErrorHandlerFunc(...)), given a *HandlerError with the kind and the operation (errors) |
OapiRequestValidator middleware from nethttp-middleware over kin-openapi |
server.validation.request: true; plain generated code, nothing to import (validation) |
| the security requirements checked by the middleware | none generated; check them in a middleware of your own |
| the status written by hand in the handler | the status of the response data, or of the error type returned (HTTP adapter) |
The scaffolds write a service stub, a middleware file and a main
once. oapi-codegen leaves these to the project.
Client
| oapi-codegen | mockzilla-codegen |
|---|---|
NewClient(server, WithHTTPClient(c), WithRequestEditorFn(fn)) |
NewClient(baseURL, WithHTTPClient(c), WithRequestEditor(fn)); the type is named by client.name |
Client.<Op>(ctx, params, body, reqEditors...) returning *http.Response |
<Op>(ctx, opts, editors...) returns the success body, and an error for any other status (methods); <Op>Request(ctx, opts, editors...) builds the request without sending it |
ClientWithResponses, <Op>WithResponse returning <Op>Response with JSON200, Body, HTTPResponse |
client.with-response: true; the same method and field names on one client type (envelopes) |
Parse<Op>Response(rsp) |
none |
<Op>Params and a body argument |
one <Op>RequestOptions struct: Query, Headers, PathParams, Cookies, Body (request options) |
reqEditors per call |
editors per call, after those of WithRequestEditor on the client |
<Op>JSONRequestBody |
<Op>RequestBody, or the referenced type; JSON appears only with several media types |
ClientInterface |
<Name>Interface, checked at compile time |
A 4xx or 5xx is an error. It has the type of models.error-mapping when the spec documents it,
and is httpclient.APIError with the status and the body otherwise.
Types
| Schema | oapi-codegen | mockzilla-codegen |
|---|---|---|
number without a format, or with an unknown one |
float32 |
float64 |
string with format uuid |
uuid.UUID |
string, checked by validation; uuid.UUID with models.format-types |
string with format email |
runtime.Email that fails JSON encoding and decoding on a bad address |
runtime.Email that decodes any string; Validate checks it |
Beyond the type mapping:
openapi_types.Date,File,EmailandUUIDareruntime.Date,runtime.File,runtime.Emailand a validatedstring.models.format-typeskeepsuuid.UUIDfor everyformat: uuid(your own type for a format).x-go-typedoes it for one schema.- A union is a struct with one field per variant, set or nil, instead of a raw
unionwithAs<Variant>,From<Variant>andMerge<Variant>(unions). - Enum constants are
<Type><Value>for every enum, not only on conflict, unlessnaming.enum-prefix: false. AdditionalPropertiesstays a map field withGetandSet, written next to the properties (additionalProperties).