Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork954
Generate Go client and server boilerplate from OpenAPI 3 specifications
License
oapi-codegen/oapi-codegen
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
oapi-codegen
is a command-line tool and library to convert OpenAPI specifications to Go code, be itserver-side implementations,API clients, or simplyHTTP models.
Usingoapi-codegen
allows you to reduce the boilerplate required to create or integrate with services based onOpenAPI 3.0, and instead focus on writing your business logic, and working on the real value-add for your organisation.
Withoapi-codegen
, there are a fewKey Design Decisions we've made, including:
- idiomatic Go, where possible
- fairly simple generated code, erring on the side of duplicate code over nicely refactored code
- supporting as much of OpenAPI 3.x as is possible, alongside Go's type system
oapi-codegen
is one part of a wider ecosystem, which can be found described in further detail in theoapi-codegen organisation on GitHub.
As announced inMay 2024,we have moved the project from the deepmap organization to our own organization, and you will need to update yourimport paths to pull updates past this point. You need to do a recursive search/replace fromgithub.com/deepmap/oapi-codegen/v2
togithub.com/oapi-codegen/oapi-codegen/v2
.
Important
oapi-codegen
moved to its new home with the version tagv2.3.0
.
If you are usingv2.2.0
or below, please install like so:
# for the binary installgo install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@v2.2.0
If you are usingv2.3.0
or above, please install like so, using the new module import path:
# for the binary installgo install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
It is recommended to followthego tool
support available from Go 1.24+ for managing the dependency ofoapi-codegen
alongside your core application.
To do this, you rungo get -tool
:
$ go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest# this will then modify your `go.mod`
From there, each invocation ofoapi-codegen
would be used like so:
//go:generate go tool oapi-codegen -config cfg.yaml ../../api.yaml
It is recommended to followthetools.go
pattern for managing the dependency ofoapi-codegen
alongside your core application.
This would give you atools/tools.go
:
//go:build tools// +build toolspackage mainimport (_"github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen")
Then, each invocation ofoapi-codegen
would be used like so:
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
Alternatively, you can install it as a binary with:
$ go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest$ oapi-codegen -version
Which then means you can invoke it like so:
//go:generate oapi-codegen --config=config.yaml ../../api.yaml
Note that you can alsomove yourtools.go
into its own sub-module to reduce the impact on your top-levelgo.mod
.
While the project does not (yet) have a defined release cadence, there may be cases where you want to pull in yet-unreleased changes to your codebase.
Therefore, you may want to pin your dependency onoapi-codegen
to a given commit hash, rather than a tag.
This isofficially recommended for consumers ofoapi-codegen
, who want features/bug fixes that haven't yet been released.
We aim to keep the default branch ready-to-release so you should be able to safely pin.
To do so, you can run:
# pin to the latest version on the default branch$ go get github.com/oapi-codegen/oapi-codegen/v2@main# alternatively, to a commit hash i.e. https://github.com/oapi-codegen/oapi-codegen/commit/71e916c59688a6379b5774dfe5904ec222b9a537$ go get github.com/oapi-codegen/oapi-codegen/v2@71e916c59688a6379b5774dfe5904ec222b9a537
This will then make a change such as:
diff --git go.mod go.modindex 44f29a4..436a780 100644--- go.mod+++ go.mod@@ -2,21 +2,20 @@-require github.com/oapi-codegen/oapi-codegen/v2 v2.1.0+require github.com/oapi-codegen/oapi-codegen/v2 v2.1.1-0.20240331212514-80f0b978ef16
oapi-codegen
is largely configured using a YAML configuration file, to simplify the number of flags that users need to remember, and to make reading thego:generate
command less daunting.
For full details of what is supported, it's worth checking outthe GoDoc forcodegen.Configuration
.
We also havea JSON Schema that can be used by IDEs/editors with the Language Server Protocol (LSP) to perform intelligent suggestions, i.e.:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:api# ...
Although we strive to retain backwards compatibility - as a project that's using a stable API per SemVer - there are sometimes opportunities we must take to fix a bug that could cause a breaking change forpeople relying upon the behaviour.
In this case, we will expose acompatibility option to restore old behaviour.
At a high level,oapi-codegen
supports:
- Generating server-side boilerplate fora number of servers (docs)
- Generating client API boilerplate (docs)
- Generating the types (docs)
- Splitting large OpenAPI specs across multiple packages(docs)
- This is also known as "Import Mapping" or "external references" across our documentation / discussion in GitHub issues
Below we can see a trimmed down example taken from the OpenAPI Petstoreexample:
// generated codetypeServerInterfaceinterface {// ...// Returns all pets// (GET /pets)FindPets(w http.ResponseWriter,r*http.Request,paramsFindPetsParams)// ...}// FindPets operation middlewarefunc (siw*ServerInterfaceWrapper)FindPets(w http.ResponseWriter,r*http.Request) {varerrerror// Parameter object where we will unmarshal all parameters from the contextvarparamsFindPetsParams// ------------- Optional query parameter "tags" -------------err=runtime.BindQueryParameter("form",true,false,"tags",r.URL.Query(),¶ms.Tags)iferr!=nil {siw.ErrorHandlerFunc(w,r,&InvalidParamFormatError{ParamName:"tags",Err:err})return}// ------------- Optional query parameter "limit" -------------err=runtime.BindQueryParameter("form",true,false,"limit",r.URL.Query(),¶ms.Limit)iferr!=nil {siw.ErrorHandlerFunc(w,r,&InvalidParamFormatError{ParamName:"limit",Err:err})return}handler:=http.Handler(http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {siw.Handler.FindPets(w,r,params)}))for_,middleware:=rangesiw.HandlerMiddlewares {handler=middleware(handler)}handler.ServeHTTP(w,r)}// HandlerWithOptions creates http.Handler with additional optionsfuncHandlerWithOptions(siServerInterface,optionsStdHTTPServerOptions) http.Handler {m:=options.BaseRouterifm==nil {m=http.NewServeMux()}ifoptions.ErrorHandlerFunc==nil {options.ErrorHandlerFunc=func(w http.ResponseWriter,r*http.Request,errerror) {http.Error(w,err.Error(),http.StatusBadRequest)}}wrapper:=ServerInterfaceWrapper{Handler:si,HandlerMiddlewares:options.Middlewares,ErrorHandlerFunc:options.ErrorHandlerFunc,}m.HandleFunc("GET "+options.BaseURL+"/pets",wrapper.FindPets)returnm}
Then, in your own code, you implement the underlying logic for theFindPets
implementation:
typePetStorestruct {Petsmap[int64]PetNextIdint64Lock sync.Mutex}// Make sure we conform to ServerInterfacevar_ServerInterface= (*PetStore)(nil)funcNewPetStore()*PetStore {return&PetStore{Pets:make(map[int64]Pet),NextId:1000,}}// FindPets implements all the handlers in the ServerInterfacefunc (p*PetStore)FindPets(w http.ResponseWriter,r*http.Request,paramsFindPetsParams) {p.Lock.Lock()deferp.Lock.Unlock()varresult []Petfor_,pet:=rangep.Pets {ifparams.Tags!=nil {// If we have tags, filter pets by tagfor_,t:=range*params.Tags {ifpet.Tag!=nil&& (*pet.Tag==t) {result=append(result,pet)}}}else {// Add all pets if we're not filteringresult=append(result,pet)}ifparams.Limit!=nil {l:=int(*params.Limit)iflen(result)>=l {// We're at the limitbreak}}}w.WriteHeader(http.StatusOK)_=json.NewEncoder(w).Encode(result)}
As we can see,oapi-codegen
simplifies some of the boilerplate by taking parameters out of the request and instead allows us to focus on the implementation.
You'll note that there's still a bit more marshaling of request/response data, which is further reduced by using theStrict server functionality.
When using the strict server, you'll have the following generated code:
// StrictServerInterface represents all server handlers.typeStrictServerInterfaceinterface {// ...// Returns all pets// (GET /pets)FindPets(ctx context.Context,requestFindPetsRequestObject) (FindPetsResponseObject,error)// ...}funcNewStrictHandlerWithOptions(ssiStrictServerInterface,middlewares []StrictMiddlewareFunc,optionsStrictHTTPServerOptions)ServerInterface {return&strictHandler{ssi:ssi,middlewares:middlewares,options:options}}// FindPets operation middlewarefunc (sh*strictHandler)FindPets(w http.ResponseWriter,r*http.Request,paramsFindPetsParams) {varrequestFindPetsRequestObjectrequest.Params=paramshandler:=func(ctx context.Context,w http.ResponseWriter,r*http.Request,requestinterface{}) (interface{},error) {returnsh.ssi.FindPets(ctx,request.(FindPetsRequestObject))}for_,middleware:=rangesh.middlewares {handler=middleware(handler,"FindPets")}response,err:=handler(r.Context(),w,r,request)iferr!=nil {sh.options.ResponseErrorHandlerFunc(w,r,err)}elseifvalidResponse,ok:=response.(FindPetsResponseObject);ok {iferr:=validResponse.VisitFindPetsResponse(w);err!=nil {sh.options.ResponseErrorHandlerFunc(w,r,err)}}elseifresponse!=nil {sh.options.ResponseErrorHandlerFunc(w,r,fmt.Errorf("unexpected response type: %T",response))}}
Then, in your own code, you implement the underlying logic for theFindPets
implementation:
// Make sure we conform to StrictServerInterfacevar_StrictServerInterface= (*PetStore)(nil)funcNewPetStore()*PetStore {return&PetStore{Pets:make(map[int64]Pet),NextId:1000,}}// FindPets implements all the handlers in the ServerInterfacefunc (p*PetStore)FindPets(ctx context.Context,requestFindPetsRequestObject) (FindPetsResponseObject,error) {p.Lock.Lock()deferp.Lock.Unlock()varresult []Petfor_,pet:=rangep.Pets {ifrequest.Params.Tags!=nil {// If we have tags, filter pets by tagfor_,t:=range*request.Params.Tags {ifpet.Tag!=nil&& (*pet.Tag==t) {result=append(result,pet)}}}else {// Add all pets if we're not filteringresult=append(result,pet)}ifrequest.Params.Limit!=nil {l:=int(*request.Params.Limit)iflen(result)>=l {// We're at the limitbreak}}}returnFindPets200JSONResponse(result),nil}
We can see that this provides the best means to focus on the implementation of the business logic within the endpoint, rather than (un)marshalling types to and from JSON, or wrangling cookies or headers.
- Produce an interface that can be satisfied by your implementation, with reduced boilerplate
- Bulk processing and parsing of OpenAPI document in Go
- Resulting output is using Go's
text/template
s, which are user-overridable - Attempts to produce Idiomatic Go
- Single-file output
- Support multiple OpenAPI files by having a package-per-OpenAPI file
- Support of OpenAPI 3.0
- OpenAPI 3.1 support isawaiting upstream support
- Note that this does not include OpenAPI 2.0 (aka Swagger)
- Extract parameters from requests, to reduce work required by your implementation
- Implicit
additionalProperties
are ignored by default (more details) - Prune unused types by default
oapi-codegen
shines by making it fairly straightforward (note that this is a purposeful choice of wording here - we want to avoid words like "easy") to generate the server-side boilerplate for a backend API.
Below you can find the supported servers, and more information about how to implement a server using them.
To provide you a fully Test Driven Development style test harness to confirm you are following the specification, you could use a tool such asopenapi.tanna.dev/go/validator, or craft your own.
Right now, we support the following servers, and are supportive of adding new servers, too!
Server | generate flag to enable code generation | Example usage |
---|---|---|
chi-server | For a Chi server, you will want a configuration file such as: # yaml-language-server: ...package:apigenerate:chi-server:truemodels:trueoutput:gen.go To implement this, check outthe Chi docs. | |
echo-server | For an Echo server, you will want a configuration file such as: # yaml-language-server: ...package:apigenerate:echo-server:truemodels:trueoutput:gen.go To implement this, check outthe Echo docs. | |
fiber-server | For a Fiber server, you will want a configuration file such as: # yaml-language-server: ...package:apigenerate:fiber-server:truemodels:trueoutput:gen.go To implement this, check outthe Fiber docs. | |
gin-server | For a Gin server, you will want a configuration file such as: # yaml-language-server: ...package:apigenerate:gin-server:truemodels:trueoutput:gen.go To implement this, check outthe Gin docs. | |
gorilla-server | For a gorilla/mux server, you will want a configuration file such as: # yaml-language-server: ...package:apigenerate:gorilla-server:truemodels:trueoutput:gen.go To implement this, check outthe gorilla/mux docs. | |
iris-server | For a Iris server, you will want a configuration file such as: # yaml-language-server: ...package:apigenerate:iris-server:truemodels:trueoutput:gen.go To implement this, check outthe Iris docs. | |
std-http-server | To use purely # yaml-language-server: ...package:apigenerate:std-http-server:truemodels:trueoutput:gen.go To implement this, check outthe Go 1.22+ |
As of Go 1.22, enhancements have been made to the routing of thenet/http
package in the standard library, which makes it a great starting point for implementing a server with, before needing to reach for another router or a full framework.
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(w http.ResponseWriter,r*http.Request)}funcHandlerFromMux(siServerInterface,mServeMux) http.Handler {returnHandlerWithOptions(si,StdHTTPServerOptions{BaseRouter:m,})}// HandlerWithOptions creates http.Handler with additional optionsfuncHandlerWithOptions(siServerInterface,optionsStdHTTPServerOptions) http.Handler {m:=options.BaseRouter// ... omitted for brevitym.HandleFunc("GET "+options.BaseURL+"/ping",wrapper.GetPing)returnm}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("encoding/json""net/http")// optional code omittedtypeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(w http.ResponseWriter,r*http.Request) {resp:=Pong{Ping:"pong",}w.WriteHeader(http.StatusOK)_=json.NewEncoder(w).Encode(resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""net/http""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/stdhttp/api")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()r:=http.NewServeMux()// get an `http.Handler` that we can useh:=api.HandlerFromMux(server,r)s:=&http.Server{Handler:h,Addr:"0.0.0.0:8080",}// And we serve HTTP until the world ends.log.Fatal(s.ListenAndServe())}
Note
This doesn't includevalidation of incoming requests.
Note
If you feel like you've done everything right, but are still receiving404 page not found
errors, make sure that you've got thego
directive in yourgo.mod
updated to:
go1.22
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(w http.ResponseWriter,r*http.Request)}// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.funcHandlerFromMux(siServerInterface,r*mux.Router) http.Handler {returnHandlerWithOptions(si,ChiServerOptions{BaseRouter:r,})}// HandlerWithOptions creates http.Handler with additional optionsfuncHandlerWithOptions(siServerInterface,optionsChiServerOptions) http.Handler {r:=options.BaseRouter// ...r.Group(func(r chi.Router) {r.Get(options.BaseURL+"/ping",wrapper.GetPing)})returnr}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("encoding/json""net/http")// optional code omittedtypeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(w http.ResponseWriter,r*http.Request) {resp:=Pong{Ping:"pong",}w.WriteHeader(http.StatusOK)_=json.NewEncoder(w).Encode(resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""net/http""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/chi/api""github.com/go-chi/chi/v5")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()r:=chi.NewMux()// get an `http.Handler` that we can useh:=api.HandlerFromMux(server,r)s:=&http.Server{Handler:h,Addr:"0.0.0.0:8080",}// And we serve HTTP until the world ends.log.Fatal(s.ListenAndServe())}
Note
This doesn't includevalidation of incoming requests.
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(w http.ResponseWriter,r*http.Request)}// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.funcHandlerFromMux(siServerInterface,r*mux.Router) http.Handler {returnHandlerWithOptions(si,GorillaServerOptions{BaseRouter:r,})}// HandlerWithOptions creates http.Handler with additional optionsfuncHandlerWithOptions(siServerInterface,optionsGorillaServerOptions) http.Handler {r:=options.BaseRouter// ...r.HandleFunc(options.BaseURL+"/ping",wrapper.GetPing).Methods("GET")returnr}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("encoding/json""net/http")// optional code omittedtypeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(w http.ResponseWriter,r*http.Request) {resp:=Pong{Ping:"pong",}w.WriteHeader(http.StatusOK)_=json.NewEncoder(w).Encode(resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""net/http""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/gorillamux/api""github.com/gorilla/mux")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()r:=mux.NewRouter()// get an `http.Handler` that we can useh:=api.HandlerFromMux(server,r)s:=&http.Server{Handler:h,Addr:"0.0.0.0:8080",}// And we serve HTTP until the world ends.log.Fatal(s.ListenAndServe())}
Note
This doesn't includevalidation of incoming requests.
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(ctx echo.Context)error}// This is a simple interface which specifies echo.Route addition functions which// are present on both echo.Echo and echo.Group, since we want to allow using// either of them for path registrationtypeEchoRouterinterface {// ...GET(pathstring,h echo.HandlerFunc,m...echo.MiddlewareFunc)*echo.Route// ...}// RegisterHandlers adds each server route to the EchoRouter.funcRegisterHandlers(routerEchoRouter,siServerInterface) {RegisterHandlersWithBaseURL(router,si,"")}// Registers handlers, and prepends BaseURL to the paths, so that the paths// can be served under a prefix.funcRegisterHandlersWithBaseURL(routerEchoRouter,siServerInterface,baseURLstring) {// ...router.GET(baseURL+"/ping",wrapper.GetPing)}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("net/http""github.com/labstack/echo/v4")// optional code omittedtypeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(ctx echo.Context)error {resp:=Pong{Ping:"pong",}returnctx.JSON(http.StatusOK,resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/echo/api""github.com/labstack/echo/v4")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()e:=echo.New()api.RegisterHandlers(e,server)// And we serve HTTP until the world ends.log.Fatal(e.Start("0.0.0.0:8080"))}
Note
This doesn't includevalidation of incoming requests.
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(c*fiber.Ctx)error}// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.funcRegisterHandlers(router fiber.Router,siServerInterface) {RegisterHandlersWithOptions(router,si,FiberServerOptions{})}// RegisterHandlersWithOptions creates http.Handler with additional optionsfuncRegisterHandlersWithOptions(router fiber.Router,siServerInterface,optionsFiberServerOptions) {// ...router.Get(options.BaseURL+"/ping",wrapper.GetPing)}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("net/http""github.com/gofiber/fiber/v2")// ensure that we've conformed to the `ServerInterface` with a compile-time checkvar_ServerInterface= (*Server)(nil)typeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(ctx*fiber.Ctx)error {resp:=Pong{Ping:"pong",}returnctx.Status(http.StatusOK).JSON(resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/fiber/api""github.com/gofiber/fiber/v2")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()app:=fiber.New()api.RegisterHandlers(app,server)// And we serve HTTP until the world ends.log.Fatal(app.Listen("0.0.0.0:8080"))}
Note
This doesn't includevalidation of incoming requests.
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(c*gin.Context)}// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.funcRegisterHandlers(router gin.IRouter,siServerInterface) {RegisterHandlersWithOptions(router,si,GinServerOptions{})}// RegisterHandlersWithOptions creates http.Handler with additional optionsfuncRegisterHandlersWithOptions(router gin.IRouter,siServerInterface,optionsGinServerOptions) {// ...router.GET(options.BaseURL+"/ping",wrapper.GetPing)}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("net/http""github.com/gin-gonic/gin")// optional code omittedtypeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(ctx*gin.Context) {resp:=Pong{Ping:"pong",}ctx.JSON(http.StatusOK,resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""net/http""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/gin/api""github.com/gin-gonic/gin")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()r:=gin.Default()api.RegisterHandlers(r,server)// And we serve HTTP until the world ends.s:=&http.Server{Handler:r,Addr:"0.0.0.0:8080",}// And we serve HTTP until the world ends.log.Fatal(s.ListenAndServe())}
Note
This doesn't includevalidation of incoming requests.
For instance, let's take this straightforward specification:
openapi:"3.0.0"info:version:1.0.0title:Minimal ping API serverpaths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'components:schemas:# base typesPong:type:objectrequired: -pingproperties:ping:type:stringexample:pong
This then generates code such as:
// Pong defines model for Pong.typePongstruct {Pingstring`json:"ping"`}// ServerInterface represents all server handlers.typeServerInterfaceinterface {// (GET /ping)GetPing(ctx iris.Context)}// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.funcRegisterHandlers(router*iris.Application,siServerInterface) {RegisterHandlersWithOptions(router,si,IrisServerOptions{})}// RegisterHandlersWithOptions creates http.Handler with additional optionsfuncRegisterHandlersWithOptions(router*iris.Application,siServerInterface,optionsIrisServerOptions) {// ...router.Get(options.BaseURL+"/ping",wrapper.GetPing)router.Build()}
To implement this HTTP server, we need to write the following code in ourapi/impl.go
:
import ("net/http""github.com/kataras/iris/v12")// optional code omittedtypeServerstruct{}funcNewServer()Server {returnServer{}}// (GET /ping)func (Server)GetPing(ctx iris.Context) {resp:=Pong{Ping:"pong",}ctx.StatusCode(http.StatusOK)_=ctx.JSON(resp)}
Now we've got our implementation, we can then write the following code to wire it up and get a running server:
import ("log""github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/iris/api""github.com/kataras/iris/v12")funcmain() {// create a type that satisfies the `api.ServerInterface`, which contains an implementation of every operation from the generated codeserver:=api.NewServer()i:=iris.Default()api.RegisterHandlers(i,server)// And we serve HTTP until the world ends.log.Fatal(i.Listen("0.0.0.0:8080"))}
Note
This doesn't includevalidation of incoming requests.
oapi-codegen
also supports generating a server that is much more strict with the contract that the implementer requires, and takes inspiration from server-side code generation for RPC servers.
This takes the boilerplate reduction from the non-strict servers and adds additional boilerplate reduction, allowing you to make the following changes to your function signatures:
-FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams)+FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
This is the highest level of strictness thatoapi-codegen
supports right now, and it's a good idea to start with this if you want the most guardrails to simplify developing your APIs.
The strict server has support for:
- multiple request/response media types and status codes on a given operation
- first-class support for
multipart/form-data
andapplication/x-www-form-urlencoded
requests - returning anHTTP 500 Internal Server Error, when an
error
is returned from a function - automagic (un)marshalling of request/responses, and setting
content-type
and HTTP status codes on responses - binding request values to a struct, a
multipart.Reader
or providing aio.Reader
You can see a little more detail of the generated code in the"What does it look like" section.
Note
To configure the strict server generation, you must specify another server to be generated. For instance:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:apigenerate:# NOTE another server must be added!chi-server:truestrict-server:trueoutput:server.gen.go
Note
This doesn't includevalidation of incoming requests.
As well as generating the server-side boilerplate,oapi-codegen
can also generate API clients.
This aims to be an API client that can be used to interact with the methods of the API, and is perfectly suited for production usage.
However, if you were looking for a slightly more SDK-style approach, or a mix of generated tests and/or documentation, this API client may not be for you, and you may want to look at alternate tooling.
For instance, given anapi.yaml
:
openapi:"3.0.0"info:version:1.0.0title:Generate modelspaths:/client:get:operationId:getClientresponses:200:content:application/json:schema:$ref:"#/components/schemas/ClientType"put:operationId:updateClientresponses:400:content:application/json:schema:type:objectproperties:code:type:stringrequired: -codecomponents:schemas:ClientType:type:objectrequired: -nameproperties:name:type:string# NOTE that this is not generated by default because it's not referenced. If you want it, you need to use the following YAML configuration:## output-options:# skip-prune: trueUnreferenced:type:objectrequired: -idproperties:id:type:integer
And acfg.yaml
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:clientoutput:client.gen.gogenerate:models:trueclient:true
And agenerate.go
:
package client//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml api.yaml
This would then generate:
package client// ...// ClientType defines model for ClientType.typeClientTypestruct {Namestring`json:"name"`}// ...// Client which conforms to the OpenAPI3 specification for this service.typeClientstruct {// The endpoint of the server conforming to this interface, with scheme,// https://api.deepmap.com for example. This can contain a path relative// to the server, such as https://api.deepmap.com/dev-test, and all the// paths in the swagger spec will be appended to the server.Serverstring// Doer for performing requests, typically a *http.Client with any// customized settings, such as certificate chains.ClientHttpRequestDoer// A list of callbacks for modifying requests which are generated before sending over// the network.RequestEditors []RequestEditorFn}// ...// The interface specification for the client above.typeClientInterfaceinterface {// GetClient requestGetClient(ctx context.Context,reqEditors...RequestEditorFn) (*http.Response,error)// UpdateClient requestUpdateClient(ctx context.Context,reqEditors...RequestEditorFn) (*http.Response,error)}// ...// ClientWithResponsesInterface is the interface specification for the client with responses above.typeClientWithResponsesInterfaceinterface {// GetClientWithResponse requestGetClientWithResponse(ctx context.Context,reqEditors...RequestEditorFn) (*GetClientResponse,error)// UpdateClientWithResponse requestUpdateClientWithResponse(ctx context.Context,reqEditors...RequestEditorFn) (*UpdateClientResponse,error)}typeGetClientResponsestruct {Body []byteHTTPResponse*http.ResponseJSON200*ClientType}// ...
With this generated client, it is then possible to construct and utilise the client, for instance:
package client_testimport ("context""fmt""log""net/http""github.com/oapi-codegen/oapi-codegen/v2/examples/client")funcTestClient_canCall() {// custom HTTP clienthc:= http.Client{}// with a raw http.Response{c,err:=client.NewClient("http://localhost:1234",client.WithHTTPClient(&hc))iferr!=nil {log.Fatal(err)}resp,err:=c.GetClient(context.TODO())iferr!=nil {log.Fatal(err)}ifresp.StatusCode!=http.StatusOK {log.Fatalf("Expected HTTP 200 but received %d",resp.StatusCode)}}// or to get a struct with the parsed response body{c,err:=client.NewClientWithResponses("http://localhost:1234",client.WithHTTPClient(&hc))iferr!=nil {log.Fatal(err)}resp,err:=c.GetClientWithResponse(context.TODO())iferr!=nil {log.Fatal(err)}ifresp.StatusCode()!=http.StatusOK {log.Fatalf("Expected HTTP 200 but received %d",resp.StatusCode())}fmt.Printf("resp.JSON200: %v\n",resp.JSON200)}}
An OpenAPI specification makes it possible to denote Servers that a client can interact with, such as:
servers:-url:https://development.gigantic-server.com/v1description:Development server-url:https://{username}.gigantic-server.com:{port}/{basePath}description:The production API servervariables:username:# note! no enum here means it is an open valuedefault:demodescription:this value is assigned by the service provider, in this example `gigantic-server.com`port:enum: -'8443' -'443'default:'8443'basePath:# open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`default:v2
It is possible to opt-in to the generation of these Server URLs with the following configuration:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:serverurlsoutput:gen.gogenerate:# NOTE that this uses default settings - if you want to use initialisms to generate i.e. `ServerURLDevelopmentServer`, you should look up the `output-options.name-normalizer` configurationserver-urls:true
This will then generate the following boilerplate:
// (the below does not include comments that are generated)constServerUrlDevelopmentServer="https://development.gigantic-server.com/v1"typeServerUrlTheProductionAPIServerBasePathVariablestringconstServerUrlTheProductionAPIServerBasePathVariableDefault="v2"typeServerUrlTheProductionAPIServerPortVariablestringconstServerUrlTheProductionAPIServerPortVariable8443ServerUrlTheProductionAPIServerPortVariable="8443"constServerUrlTheProductionAPIServerPortVariable443ServerUrlTheProductionAPIServerPortVariable="443"constServerUrlTheProductionAPIServerPortVariableDefaultServerUrlTheProductionAPIServerPortVariable=ServerUrlTheProductionAPIServerPortVariable8443typeServerUrlTheProductionAPIServerUsernameVariablestringconstServerUrlTheProductionAPIServerUsernameVariableDefault="demo"funcServerUrlTheProductionAPIServer(basePathServerUrlTheProductionAPIServerBasePathVariable,portServerUrlTheProductionAPIServerPortVariable,usernameServerUrlTheProductionAPIServerUsernameVariable) (string,error) {// ...}
Notice that for URLs that are not templated, a simpleconst
definition is created.
However, for more complex URLs that definedvariables
in them, we generate the types (and anyenum
values ordefault
values), and instead use a function to create the URL.
For a complete example seeexamples/generate/serverurls
.
When generating the types for interacting with the generated client,oapi-codegen
will use theoperationId
and add on aRequest
orResponse
suffix.
However, this can clash if you have named your component schemas in a similar way.
For instance:
openapi:"3.0.0"info:version:1.0.0title:"Show that generated client boilerplate can clash if schemas are well named i.e. `*Request` and `*Response`"paths:/client:put:operationId:updateClientrequestBodies:application/json:schema:$ref:'#/components/schemas/UpdateClientRequest'responses:400:content:application/json:schema:$ref:'#/components/schemas/UpdateClientResponse'components:schemas:UpdateClientRequest:type:objectproperties:code:type:stringrequired: -codeUpdateClientResponse:type:objectrequired: -nameproperties:name:type:string
If you were to generate with this configuration:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:clientoutput:client.gen.gogenerate:models:trueclient:true
This would then result ingo build
failures:
# github.com/oapi-codegen/oapi-codegen/v2/examples/clienttypenameclash./client.gen.go:184:6: UpdateClientResponse redeclared in this block ./client.gen.go:17:6: other declaration of UpdateClientResponse./client.gen.go:192:7: r.HTTPResponse undefined (type UpdateClientResponse has no field or method HTTPResponse)./client.gen.go:193:12: r.HTTPResponse undefined (type UpdateClientResponse has no field or method HTTPResponse)./client.gen.go:200:7: r.HTTPResponse undefined (type UpdateClientResponse has no field or method HTTPResponse)./client.gen.go:201:12: r.HTTPResponse undefined (type UpdateClientResponse has no field or method HTTPResponse)./client.gen.go:224:3: unknown field Body in struct literal of type UpdateClientResponse./client.gen.go:225:3: unknown field HTTPResponse in struct literal of type UpdateClientResponse./client.gen.go:234:12: response.JSON400 undefined (type *UpdateClientResponse has no field or method JSON400)
To fix this, use theresponse-type-suffix
Output Option:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json package: client output: client.gen.go generate: models: true client: true+output-options:+ response-type-suffix: Resp
This will then rename the generated types from the default to use the new suffix:
-type UpdateClientResponse struct {+type UpdateClientResp struct { Body []byte HTTPResponse *http.Response JSON400 *UpdateClientResponse }
There is no currently planned work to change this behaviour.
If you're looking to only generate the models for interacting with a remote service, for instance if you need to hand-roll the API client for whatever reason, you can do this as-is.
Tip
Try to define as much as possible within the#/components/schemas
object, asoapi-codegen
will generate all the types here.
Although we can generate some types based on inline definitions in i.e. a path's response type, it isn't always possible to do this, or if it is generated, can be a little awkward to work with as it may be defined as an anonymous struct.
For instance, given anapi.yaml
:
openapi:"3.0.0"info:version:1.0.0title:Generate modelspaths:/client:get:operationId:getClientresponses:200:content:application/json:schema:# NOTE that Client is generated here, because it's within #/components/schemas$ref:"#/components/schemas/Client"put:operationId:updateClientresponses:400:content:application/json:# NOTE that this anonymous object is /not/ generated because it's an anonymous, but would be generated if using `generate: client`# See https://github.com/oapi-codegen/oapi-codegen/issues/1512schema:type:objectproperties:code:type:stringrequired: -codecomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:string# NOTE that this is not generated by default because it's not referenced. If you want it, you need to use the following YAML configuration:## output-options:# skip-prune: trueUnreferenced:type:objectrequired: -idproperties:id:type:integer
And acfg.yaml
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:onlymodelsoutput:only-models.gen.gogenerate:models:true
And agenerate.go
:
package onlymodels//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml api.yaml
This would then generate:
package onlymodels// Client defines model for Client.typeClientstruct {Namestring`json:"name"`}
If you wish to also generate theUnreferenced
type, you would need the followingcfg.yaml
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:onlymodelsoutput:only-models.gen.gogenerate:models:trueoutput-options:# NOTE that this is only required for the `Unreferenced` typeskip-prune:true
For a complete example seeexamples/only-models
.
Splitting large OpenAPI specs across multiple packages (aka "Import Mapping" or "external references")
When you've got a large OpenAPI specification, you may find it useful to split the contents of the spec across multiple files, using external references, such as:
responses:200:description:Successcontent:application/json:schema:$ref:'#/components/schemas/User'
This is supported byoapi-codegen
, through the ability to perform "Import Mapping".
For instance, let's say that we have a large API, which has a user-facing API and an admin API, both of which use a common set of API models.
In this case, we may have an Admin API that looks like:
# admin/api.yamlopenapi:"3.0.0"info:version:1.0.0title:Admin APIdescription:The admin-only portion of the API, which has its own separate OpenAPI spectags: -name:admindescription:Admin API endpoints -name:userdescription:API endpoint that pertains to user datapaths:/admin/user/{id}:get:tags: -admin -usersummary:Get a user's detailsoperationId:getUserByIdparameters: -name:idin:pathrequired:trueschema:type:stringformat:uuidresponses:200:description:Successcontent:application/json:schema:$ref:'../common/api.yaml#/components/schemas/User'
This references the common spec:
# common/api.yamlcomponents:schemas:User:type:objectadditionalProperties:falseproperties:name:type:stringrequired: -name
So how do we getoapi-codegen
to generate our code?
Tip
Sinceoapi-codegen
v2.4.0, it is now possible to split large OpenAPI specifications into the same Go package, using the "self" mapping (denoted by a-
) when using Import Mapping.
This is an improvement on the previous model, which would require splitting files across multiple packages.
Note
You still need to have multiplego generate
s, and any other configuration files.
To getoapi-codegen
's single-package support working, we need multiple calls tooapi-codegen
, one call per OpenAPI spec file:
$ go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg-api.yaml ../admin/api.yaml$ go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg-user.yaml ../common/api.yaml
This therefore means that we need multiple configuration files, such ascfg-api.yaml
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:samepackageoutput:server.gen.gogenerate:models:truechi-server:truestrict-server:trueoutput-options:# to make sure that all types are generatedskip-prune:trueimport-mapping:user.yaml:"-"
And then ourcfg-user.yaml
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:samepackageoutput:user.gen.gogenerate:models:trueoutput-options:# to make sure that all types are generatedskip-prune:true
From here,oapi-codegen
will generate multiple Go files, all within the same package, which can be used to break down your large OpenAPI specifications, and generate only the subsets of code needed for each part of the spec.
Check outthe import-mapping/samepackage example for the full code.
To getoapi-codegen
's multi-package support working, we need to set up our directory structure:
├── admin│ ├── cfg.yaml│ └── generate.go└── common ├── cfg.yaml └── generate.go
We could start with our configuration file for our admin API spec:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json# admin/cfg.yamlpackage:adminoutput:server.gen.gogenerate:models:truechi-server:trueoutput-options:# to make sure that all types are generatedskip-prune:true# NOTE that this won't work, as it's missing `import-mapping`
If we were to runoapi-codegen
, this will fail with the following error
error generating code: error creating operation definitions: error generating response definitions: error generating request body definition: error turning reference (../common/api.yaml#/components/schemas/User) into a Go type: unrecognized external reference '../common/api.yaml'; please provide the known import for this reference using option --import-mapping
This is becauseoapi-codegen
requires theimport-mapping
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:adminoutput:server.gen.gogenerate:models:truechi-server:trueoutput-options:# to make sure that all types are generatedskip-prune:trueimport-mapping:# for a given file/URL that is $ref'd, point `oapi-codegen` to the Go package that this spec is generated into, to perform Go package imports../common/api.yaml:github.com/oapi-codegen/oapi-codegen/v2/examples/import-mapping/common
This will then generate the following code:
package adminimport (// ...externalRef0"github.com/oapi-codegen/oapi-codegen/v2/examples/import-mapping/common")// User defines model for User.typeUser= externalRef0.User
If you don't want to do this, an alternate option is touse a single package, with multiple OpenAPI spec files for that given package or tobundle your multiple OpenAPI files into a single spec.
Check outthe import-mapping/multiplepackages example for the full code.
Prior tooapi-codegen
v2.4.0, users wishing to override specific configuration, for instance taking advantage of extensions such asx-go-type
would need to modify the OpenAPI specification they are using.
In a lot of cases, this OpenAPI specification would be produced by a different team to the consumers (or even a different company) and so asking them to make changes like this were unreasonable.
This would lead to the API consumers needing to vendor the specification from the producer (which isour recommendation anyway) and then make any number of local changes to the specification to make it generate code that looks reasonable.
However, in the case that a consumer would update their specification, they would likely end up with a number of merge conflicts.
Now, as ofoapi-codegen
v2.4.0, it is now possible to make changes to the input OpenAPI specificationwithout needing to modify it directly.
This takes advantage of theOpenAPI Overlay specification, which is a stable specification.
Caution
Beware! Here (may) be dragons.
The Overlay specification requires the use of JSON Path, which some users may find difficult to write and/or maintain.
We still heavily recommend using Overlay functionality, but would like users to be aware of this.
There is aproposed modification to the specification which would relax the need for JSON Path as the targeting mechanism.
For instance, let's say that we have the following OpenAPI specification, which provides insight into an internal endpoint that we should not be generating any code for (denoted byx-internal
):
openapi:"3.0.0"info:version:1.0.0title:"Example to indicate how to use the OpenAPI Overlay specification (https://github.com/OAI/Overlay-Specification)"paths:/ping:get:responses:'200':description:pet responsecontent:application/json:schema:$ref:'#/components/schemas/Pong'delete:x-internal:trueresponses:'202':content:{}
If we were to runoapi-codegen
with out-of-the-box functionality, this would then lead to the DELETE endpoint being generated, which we don't want.
Instead, we can define the followingoverlay.yaml
:
overlay:1.0.0info:title:Overlayversion:0.0.0actions:-target:"$"description:Perform a structural overlay, which can be more readable, as it's clear what the shape of the document isupdate:info:x-overlay-applied:structured-overlaypaths:/ping:get:responses:'200':description:Perform a ping request-target:$.paths.*[?(@.x-internal)]description:Remove internal endpoints (noted by x-internal)remove:true-target:$.paths.*.*[?(@.x-internal)]description:Remove internal endpoints (noted by x-internal)remove:true
And our configuration file foroapi-codegen
:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:apioutput:ping.gen.gogenerate:models:truegorilla-server:trueembedded-spec:trueoutput-options:overlay:path:overlay.yaml
This then completely removes the DELETE endpointbefore we even start to parse the specification inoapi-codegen
, so it's as if your specification was provided without that endpoint.
Additionally, we can override other pieces of metadata, such as the description for operations.
Check outthe overlay example for the full code, and some more complex examples.
It's possible that you want to be able to determine whether a field isn't sent, is sent asnull
or has a value.
For instance, if you had the following OpenAPI property:
S:type:objectproperties:Field:type:stringnullable:truerequired:[]
The default behaviour inoapi-codegen
is to generate:
typeSstruct {Field*string`json:"field,omitempty"`}
However, you lose the ability to understand the three cases, as there's no way to distinguish two of the types from each other:
- is this field not sent? (Can be checked with
S.Field == nil
) - is this field
null
? (Can be checked withS.Field == nil
) - does this field have a value? (
S.Field != nil && *S.Field == "123"
)
As ofoapi-codegen
v2.1.0 it is now possible to represent this with thenullable.Nullable
type fromour new library, oapi-codegen/nullable.
If you configure your generator's Output Options to opt-in to this behaviour, as so:
output-options:nullable-type:true
You will now receive the following output:
typeSstruct {// note that there's no pointer here, just `omitempty`Field nullable.Nullable[string]`json:"field,omitempty"`}
As well as the core OpenAPI support, we also support the following OpenAPI extensions, as denoted by theOpenAPI Specification Extensions.
The following extensions are supported:
Extension | Description |
---|---|
| Override the generated type definition (and optionally, add an import from another package) |
| Do not add a pointer type for optional fields in structs |
| Override the generated name of a field or a type |
| Override the generated name of a type |
| Force the presence of the JSON tag `omitempty` on a field |
| Force the presence of the JSON tag `omitzero` on a field |
| When (un)marshaling JSON, ignore field(s) |
| Generate arbitrary struct tags to fields |
| Override generated variable names for enum constants |
| Add a GoDoc deprecation warning to a type |
| Explicitly order struct fields |
| Only honour the `x-go-name` when generating field names |
x-go-type
/x-go-type-import
- override the generated type definition (and optionally, add an import from another package)
Using thex-go-type
(and optionally,x-go-type-import
when you need to import another package) allows overriding the type thatoapi-codegen
determined the generated type should be.
We can see this at play with the following schemas:
components:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:objectrequired: -nameproperties:name:type:string# this is a bit of a contrived example, as you could instead use# `format: uuid` but it explains how you'd do this when there may be# a clash, for instance if you already had a `uuid` package that was# being imported, or ...x-go-type:googleuuid.UUIDx-go-type-import:path:github.com/google/uuidname:googleuuidid:type:number# ... this is also a bit of a contrived example, as you could use# `type: integer` but in the case that you know better than what# oapi-codegen is generating, like so:x-go-type:int64
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Id*int64`json:"id,omitempty"`Name googleuuid.UUID`json:"name"`}
You can see this in more detail inthe example code.
Tip
If you prefer this behaviour, and prefer to not have to annotate your whole OpenAPI spec for this behaviour, you can useoutput-options.prefer-skip-optional-pointer=true
to default this behaviour for all fields.
It is then possible to override this on a per-type/per-field basis where necessary.
By default,oapi-codegen
will generate a pointer for optional fields.
Using thex-go-type-skip-optional-pointer
extension allows omitting that pointer.
We can see this at play with the following schemas:
components:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:objectrequired: -nameproperties:name:type:stringid:type:numberx-go-type-skip-optional-pointer:true
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Idfloat32`json:"id,omitempty"`Namestring`json:"name"`}
You can see this in more detail inthe example code.
By default,oapi-codegen
will attempt to generate the name of fields and types in as best a way it can.
However, sometimes, the name doesn't quite fit what your codebase standards are, or the intent of the field, so you can override it withx-go-name
.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-go-namecomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:object# can be used on a typex-go-name:ClientRenamedByExtensionrequired: -nameproperties:name:type:stringid:type:number# or on a fieldx-go-name:AccountIdentifier
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientRenamedByExtension defines model for ClientWithExtension.typeClientRenamedByExtensionstruct {AccountIdentifier*float32`json:"id,omitempty"`Namestring`json:"name"`}
You can see this in more detail inthe example code.
Note
Notice that this is subtly different to thex-go-name
, which also applies tofields withinstruct
s.
By default,oapi-codegen
will attempt to generate the name of types in as best a way it can.
However, sometimes, the name doesn't quite fit what your codebase standards are, or the intent of the field, so you can override it withx-go-name
.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-go-type-namecomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:objectx-go-type-name:ClientRenamedByExtensionrequired: -nameproperties:name:type:stringid:type:number# NOTE attempting a `x-go-type-name` here is a no-op, as we're not producing a _type_ only a _field_x-go-type-name:ThisWillNotBeUsed
From here, we now get two different models and a type alias:
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtension=ClientRenamedByExtension// ClientRenamedByExtension defines model for .typeClientRenamedByExtensionstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}
You can see this in more detail inthe example code.
In a case that you may want to add the JSON struct tagomitempty
to types that don't have one generated by default - for instance a required field - you can use thex-omitempty
extension.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-omitemptycomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:objectrequired: -nameproperties:name:type:string# for some reason, you may want this behaviour, even though it's a required fieldx-omitempty:trueid:type:number
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name,omitempty"`}
You can see this in more detail inthe example code.
Note
omitzero
was added in Go 1.24. If you're not using Go 1.24 in your project, this won't work.
In a case that you may want to add the JSON struct tagomitzero
to types, you can use thex-omitempty
extension.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-omitemptycomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:objectrequired: -nameproperties:name:type:stringid:type:numberx-omitzero:true
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Id*float32`json:"id,omitempty,omitzero"`Namestring`json:"name"`}
You can see this in more detail inthe example code.
By default,oapi-codegen
will generatejson:"..."
struct tags for all fields in a struct, so JSON (un)marshaling works.
However, sometimes, you want to omit fields, which can be done with thex-go-json-ignore
extension.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-go-json-ignorecomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:stringcomplexField:type:objectproperties:name:type:stringaccountName:type:string# ...ClientWithExtension:type:objectrequired: -nameproperties:name:type:stringcomplexField:type:objectproperties:name:type:stringaccountName:type:string# ...x-go-json-ignore:true
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {ComplexField*struct {AccountName*string`json:"accountName,omitempty"`Name*string`json:"name,omitempty"`}`json:"complexField,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {ComplexField*struct {AccountName*string`json:"accountName,omitempty"`Name*string`json:"name,omitempty"`}`json:"-"`Namestring`json:"name"`}
Notice that theComplexField
is still generated in full, but the type will then be ignored with JSON marshalling.
You can see this in more detail inthe example code.
If you're making use of a field's struct tags to i.e. apply validation, decide whether something should be logged, etc, you can usex-oapi-codegen-extra-tags
to set additional tags for your generated types.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-oapi-codegen-extra-tagscomponents:schemas:Client:type:objectrequired: -name -idproperties:name:type:stringid:type:numberClientWithExtension:type:objectrequired: -name -idproperties:name:type:stringid:type:numberx-oapi-codegen-extra-tags:validate:"required,min=1,max=256"safe-to-log:"true"gorm:primarykey
From here, we now get two different models:
// Client defines model for Client.typeClientstruct {Idfloat32`json:"id"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Idfloat32`gorm:"primarykey" json:"id" safe-to-log:"true" validate:"required,min=1,max=256"`Namestring`json:"name"`}
You can see this in more detail inthe example code.
When consuming an enum value from an external system, the name may not produce a nice variable name. Using thex-enum-varnames
extension allows overriding the name of the generated variable names.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-enumNames and x-enum-varnamescomponents:schemas:ClientType:type:stringenum: -ACT -EXPClientTypeWithNamesExtension:type:stringenum: -ACT -EXPx-enumNames: -Active -ExpiredClientTypeWithVarNamesExtension:type:stringenum: -ACT -EXPx-enum-varnames: -Active -Expired
From here, we now get two different forms of the same enum definition.
// Defines values for ClientType.const (ACTClientType="ACT"EXPClientType="EXP")// Defines values for ClientTypeWithNamesExtension.const (ClientTypeWithNamesExtensionActiveClientTypeWithNamesExtension="ACT"ClientTypeWithNamesExtensionExpiredClientTypeWithNamesExtension="EXP")// Defines values for ClientTypeWithVarNamesExtension.const (ClientTypeWithVarNamesExtensionActiveClientTypeWithVarNamesExtension="ACT"ClientTypeWithVarNamesExtensionExpiredClientTypeWithVarNamesExtension="EXP")// ClientType defines model for ClientType.typeClientTypestring// ClientTypeWithNamesExtension defines model for ClientTypeWithNamesExtension.typeClientTypeWithNamesExtensionstring// ClientTypeWithVarNamesExtension defines model for ClientTypeWithVarNamesExtension.typeClientTypeWithVarNamesExtensionstring
You can see this in more detail inthe example code.
When an OpenAPI type is deprecated, a deprecation warning can be added in the GoDoc usingx-deprecated-reason
.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-deprecated-reasoncomponents:schemas:Client:type:objectrequired: -nameproperties:name:type:stringid:type:numberClientWithExtension:type:objectrequired: -nameproperties:name:type:stringdeprecated:truex-deprecated-reason:Don't use because reasonsid:type:number# NOTE that this doesn't generate, as no `deprecated: true` is setx-deprecated-reason:NOTE you shouldn't see this, as you've not deprecated this field
From here, we now get two different forms of the same enum definition.
// Client defines model for Client.typeClientstruct {Id*float32`json:"id,omitempty"`Namestring`json:"name"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Id*float32`json:"id,omitempty"`// Deprecated: Don't use because reasonsNamestring`json:"name"`}
Notice that because we've not setdeprecated: true
to thename
field, it doesn't generate a deprecation warning.
You can see this in more detail inthe example code.
Whether you like certain fields being ordered before others, or you want to perform more efficient packing of your structs, thex-order
extension is here for you.
Note thatx-order
is 1-indexed -x-order: 0
is not a valid value.
We can see this at play with the following schemas:
openapi:"3.0.0"info:version:1.0.0title:x-ordercomponents:schemas:Client:type:objectrequired: -nameproperties:a_name:type:stringid:type:numberClientWithExtension:type:objectrequired: -nameproperties:a_name:type:stringx-order:2id:type:numberx-order:1
From here, we now get two different forms of the same type definition.
// Client defines model for Client.typeClientstruct {AName*string`json:"a_name,omitempty"`Id*float32`json:"id,omitempty"`}// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {Id*float32`json:"id,omitempty"`AName*string`json:"a_name,omitempty"`}
You can see this in more detail inthe example code.
Warning
Using this option may lead to cases whereoapi-codegen
's rewriting of field names to prevent clashes with other types, or to prevent including characters that may not be valid Go field names.
In some cases, you may not want use the inbuilt options for converting an OpenAPI field name to a Go field name, such as thename-normalizer: "ToCamelCaseWithInitialisms"
, and instead trust the name that you've defined for the type better.
In this case, you can usex-oapi-codegen-only-honour-go-name
to enforce this, alongside specifying theallow-unexported-struct-field-names
compatibility option.
This allows you to take a spec such as:
openapi:"3.0.0"info:version:1.0.0title:x-oapi-codegen-only-honour-go-namecomponents:schemas:TypeWithUnexportedField:description:A struct will be output where one of the fields is not exportedproperties:name:type:stringid:type:string# NOTE that there is an explicit usage of a lowercase characterx-go-name:accountIdentifierx-oapi-codegen-extra-tags:json:"-"x-oapi-codegen-only-honour-go-name:true
And we'll generate:
// TypeWithUnexportedField A struct will be output where one of the fields is not exportedtypeTypeWithUnexportedFieldstruct {accountIdentifier*string`json:"-"`Name*string`json:"name,omitempty"`}
You can see this in more detail inthe example code.
The generated code thatoapi-codegen
produces has some validation for some incoming data, such as checking for required headers, and when using thestrict server you get some more validation around the correct usage of the response types.
However, this leaves a lot of validation that needs to be done, which can be tedious to hand-write this logic, especially for large or complex OpenAPI specifications.
To simplify this, we use a middleware, which provides the request validation. The middleware you want to use depends on the server you're using:
Server | Middleware library |
---|---|
Any other server (which conforms to |
Note
It isnot currently possible to validate the HTTP response with a middleware.
Note
We're alsoexploring the use oflibopenapi-validator for request/response validation middleware
If you're using a specification withSecurity Schemes andSecurity Requirements, you'll want to authenticate and authorize requests.
Note
Out-of-the-box, the server-side code generated byoapi-codegen
does not provide security validation.
To perform authentication, you will need to use thevalidation middleware.
In the future, we plan toimplement server-side validation in the generated code
To see how this can work, check out theauthenticated API example.
With a generated client, you'll want to use the client's generatedWithRequestEditorFn
function to pass in a given request editorRequestEditorFn
.
For instance:
import ("context""fmt""log""github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider")funcmain() {basicAuth,err:=securityprovider.NewSecurityProviderBasicAuth("my_user","my_pass")iferr!=nil {log.Fatal(err)}client,err:=NewClient("https://....",WithRequestEditorFn(basicAuth.Intercept))iferr!=nil {log.Fatal(err)}resp,err:=client.GetClient(context.TODO())iferr!=nil {log.Fatal(err)}fmt.Printf("resp.StatusCode: %v\n",resp.StatusCode)}
Notice that we're using a pre-built provider from thepkg/securityprovider
package, which has some inbuilt support for other types of authentication, too.
It is possible to extend the inbuilt code generation fromoapi-codegen
using Go'stext/template
s.
You can specify, through your configuration file, theoutput-options.user-templates
setting to override the inbuilt templates and use a user-defined template.
Note
Filenames given to theuser-templates
configuration mustexactly match the filename thatoapi-codegen
is looking for
Within your configuration file, you can specify relative or absolute paths to a file to reference for the template, such as:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json# ...output-options:user-templates:client-with-responses.tmpl:./custom-template.tmpladditional-properties.tmpl:/tmp/foo.bartypedef.tmpl:no-prefix.tmpl
Warning
We do not interpolate~
or$HOME
(or other environment variables) in paths given
It is also possible to use HTTPS URLs.
Warning
Although possible, this does lead tooapi-codegen
executions not necessarily being reproducible. It's recommended to vendor (copy) the OpenAPI spec into your codebase and reference it locally
Seethis blog post for an example of how to use GitHub Actions to manage the updates of files across repos
This will be disabled by default (but possible to turn back on via configuration)in the future
To use it, you can use the following configuration:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json# ...output-options:user-templates:# The following are referencing a version of the default client-with-responses.tmpl file, but loaded in through GitHub's raw.githubusercontent.com. The general form to use raw.githubusercontent.com is as follows https://raw.githubusercontent.com/<username>/<project>/<commitish>/path/to/template/template.tmpl# Alternatively using raw.githubusercontent.com with a hashclient-with-responses.tmpl:https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/ad5eada4f3ccc28a88477cef62ea21c17fc8aa01/pkg/codegen/templates/client-with-responses.tmpl# Alternatively using raw.githubusercontent.com with a tagclient-with-responses.tmpl:https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/v2.1.0/pkg/codegen/templates/client-with-responses.tmpl# Alternatively using raw.githubusercontent.com with a branchclient-with-responses.tmpl:https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/master/pkg/codegen/templates/client-with-responses.tmpl
Warning
If using URLs that pull locations from a Git repo, such asraw.githubusercontent.com
, it is strongly encouraged to use a tag or a raw commit hash instead of a branch likemain
. Tracking a branch can lead to unexpected API drift, and loss of the ability to reproduce a build.
It's also possible to set the templates inline in the configuration file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json# ...output-options:user-templates:# NOTE the use of the `|` (pipe symbol) here to denote that this is a# multi-line statement that should preserve newlines. More reading:# https://stackoverflow.com/a/18708156/2257038 and# https://stackoverflow.com/a/15365296/2257038client-with-responses.tmpl:| // ClientWithResponses builds on ClientInterface to offer response payloads type ClientWithResponses struct { ClientInterface } ...
Alternatively, you are able to use the underlying code generation as a package, whichwill be documented in the future.
OpenAPI Schemas implicitly acceptadditionalProperties
, meaning that any fields provided, but not explicitly defined via properties on the schema are accepted as input, and propagated. When unspecified, OpenAPI defines that theadditionalProperties
field is assumed to betrue
.
For simplicity, and to remove a fair bit of duplication and boilerplate,oapi-codegen
decides to ignore the implicitadditionalProperties: true
, and instead requires you to specify theadditionalProperties
key to generate the boilerplate.
Note
In the futurethis will be possible to disable this functionality, and honour the implicitadditionalProperties: true
Below you can see some examples of howadditionalProperties
affects the generated code.
components:schemas:Thing:type:objectrequired: -idproperties:id:type:integer# implicit additionalProperties: true
Will generate:
// Thing defines model for Thing.typeThingstruct {Idint`json:"id"`}// with no generated boilerplate nor the `AdditionalProperties` field
components:schemas:Thing:type:objectrequired: -idproperties:id:type:integer# explicit trueadditionalProperties:true
Will generate:
// Thing defines model for Thing.typeThingstruct {Idint`json:"id"`AdditionalPropertiesmap[string]interface{}`json:"-"`}// with generated boilerplate below
Boilerplate
// Getter for additional properties for Thing. Returns the specified// element and whether it was foundfunc (aThing)Get(fieldNamestring) (valueinterface{},foundbool) {ifa.AdditionalProperties!=nil {value,found=a.AdditionalProperties[fieldName]}return}// Setter for additional properties for Thingfunc (a*Thing)Set(fieldNamestring,valueinterface{}) {ifa.AdditionalProperties==nil {a.AdditionalProperties=make(map[string]interface{})}a.AdditionalProperties[fieldName]=value}// Override default JSON handling for Thing to handle AdditionalPropertiesfunc (a*Thing)UnmarshalJSON(b []byte)error {object:=make(map[string]json.RawMessage)err:=json.Unmarshal(b,&object)iferr!=nil {returnerr}ifraw,found:=object["id"];found {err=json.Unmarshal(raw,&a.Id)iferr!=nil {returnfmt.Errorf("error reading 'id': %w",err)}delete(object,"id")}iflen(object)!=0 {a.AdditionalProperties=make(map[string]interface{})forfieldName,fieldBuf:=rangeobject {varfieldValinterface{}err:=json.Unmarshal(fieldBuf,&fieldVal)iferr!=nil {returnfmt.Errorf("error unmarshaling field %s: %w",fieldName,err)}a.AdditionalProperties[fieldName]=fieldVal}}returnnil}// Override default JSON handling for Thing to handle AdditionalPropertiesfunc (aThing)MarshalJSON() ([]byte,error) {varerrerrorobject:=make(map[string]json.RawMessage)object["id"],err=json.Marshal(a.Id)iferr!=nil {returnnil,fmt.Errorf("error marshaling 'id': %w",err)}forfieldName,field:=rangea.AdditionalProperties {object[fieldName],err=json.Marshal(field)iferr!=nil {returnnil,fmt.Errorf("error marshaling '%s': %w",fieldName,err)}}returnjson.Marshal(object)}
components:schemas:Thing:type:objectrequired: -idproperties:id:type:integer# simple typeadditionalProperties:type:integer
Will generate:
// Thing defines model for Thing.typeThingstruct {Idint`json:"id"`AdditionalPropertiesmap[string]int`json:"-"`}// with generated boilerplate below
Boilerplate
// Getter for additional properties for Thing. Returns the specified// element and whether it was foundfunc (aThing)Get(fieldNamestring) (valueint,foundbool) {ifa.AdditionalProperties!=nil {value,found=a.AdditionalProperties[fieldName]}return}// Setter for additional properties for Thingfunc (a*Thing)Set(fieldNamestring,valueint) {ifa.AdditionalProperties==nil {a.AdditionalProperties=make(map[string]int)}a.AdditionalProperties[fieldName]=value}// Override default JSON handling for Thing to handle AdditionalPropertiesfunc (a*Thing)UnmarshalJSON(b []byte)error {object:=make(map[string]json.RawMessage)err:=json.Unmarshal(b,&object)iferr!=nil {returnerr}ifraw,found:=object["id"];found {err=json.Unmarshal(raw,&a.Id)iferr!=nil {returnfmt.Errorf("error reading 'id': %w",err)}delete(object,"id")}iflen(object)!=0 {a.AdditionalProperties=make(map[string]int)forfieldName,fieldBuf:=rangeobject {varfieldValinterr:=json.Unmarshal(fieldBuf,&fieldVal)iferr!=nil {returnfmt.Errorf("error unmarshaling field %s: %w",fieldName,err)}a.AdditionalProperties[fieldName]=fieldVal}}returnnil}// Override default JSON handling for Thing to handle AdditionalPropertiesfunc (aThing)MarshalJSON() ([]byte,error) {varerrerrorobject:=make(map[string]json.RawMessage)object["id"],err=json.Marshal(a.Id)iferr!=nil {returnnil,fmt.Errorf("error marshaling 'id': %w",err)}forfieldName,field:=rangea.AdditionalProperties {object[fieldName],err=json.Marshal(field)iferr!=nil {returnnil,fmt.Errorf("error marshaling '%s': %w",fieldName,err)}}returnjson.Marshal(object)}
components:schemas:Thing:type:objectrequired: -idproperties:id:type:integer# objectadditionalProperties:type:objectproperties:foo:type:string
Will generate:
// Thing defines model for Thing.typeThingstruct {Idint`json:"id"`AdditionalPropertiesmap[string]struct {Foo*string`json:"foo,omitempty"`}`json:"-"`}// with generated boilerplate below
Boilerplate
// Getter for additional properties for Thing. Returns the specified// element and whether it was foundfunc (aThing)Get(fieldNamestring) (valuestruct {Foo*string`json:"foo,omitempty"`},foundbool) {ifa.AdditionalProperties!=nil {value,found=a.AdditionalProperties[fieldName]}return}// Setter for additional properties for Thingfunc (a*Thing)Set(fieldNamestring,valuestruct {Foo*string`json:"foo,omitempty"`}) {ifa.AdditionalProperties==nil {a.AdditionalProperties=make(map[string]struct {Foo*string`json:"foo,omitempty"`})}a.AdditionalProperties[fieldName]=value}// Override default JSON handling for Thing to handle AdditionalPropertiesfunc (a*Thing)UnmarshalJSON(b []byte)error {object:=make(map[string]json.RawMessage)err:=json.Unmarshal(b,&object)iferr!=nil {returnerr}ifraw,found:=object["id"];found {err=json.Unmarshal(raw,&a.Id)iferr!=nil {returnfmt.Errorf("error reading 'id': %w",err)}delete(object,"id")}iflen(object)!=0 {a.AdditionalProperties=make(map[string]struct {Foo*string`json:"foo,omitempty"`})forfieldName,fieldBuf:=rangeobject {varfieldValstruct {Foo*string`json:"foo,omitempty"`}err:=json.Unmarshal(fieldBuf,&fieldVal)iferr!=nil {returnfmt.Errorf("error unmarshaling field %s: %w",fieldName,err)}a.AdditionalProperties[fieldName]=fieldVal}}returnnil}// Override default JSON handling for Thing to handle AdditionalPropertiesfunc (aThing)MarshalJSON() ([]byte,error) {varerrerrorobject:=make(map[string]json.RawMessage)object["id"],err=json.Marshal(a.Id)iferr!=nil {returnnil,fmt.Errorf("error marshaling 'id': %w",err)}forfieldName,field:=rangea.AdditionalProperties {object[fieldName],err=json.Marshal(field)iferr!=nil {returnnil,fmt.Errorf("error marshaling '%s': %w",fieldName,err)}}returnjson.Marshal(object)}
One of the key thingsoapi-codegen
does is to use an "optional pointer", following idiomatic Go practices, to indicate that a field/type is optional.
This can be tuned on a per-field basis, using thex-go-type-skip-optional-pointer
extension, but it can be a bit repetitive, or can be more complex when using an OpenAPI Overlay.
As ofoapi-codegen
v2.5.0, this can be tuned in two specific ways, via the followingoutput-options:
:
prefer-skip-optional-pointer
: a global default that you donot want the "optional pointer" generated. Optional fields will not have an "optional pointer", and will have anomitempty
JSON tagprefer-skip-optional-pointer-with-omitzero
: when used in conjunction withprefer-skip-optional-pointer
, any optional fields are generated with anomitzero
JSON tag.Requires Go 1.24+
In both cases, there is control on a per-field level to setx-go-type-skip-optional-pointer: false
orx-omitzero: false
to undo these to field(s).
For example, when combining both options:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonpackage:preferskipoptionalpointerwithomitzerooutput:gen.gogenerate:# ...output-options:# ...prefer-skip-optional-pointer:trueprefer-skip-optional-pointer-with-omitzero:true
When we have the following spec:
openapi:"3.0.0"info:version:1.0.0title:prefer-skip-optional-pointer-with-omitzerocomponents:schemas:ClientWithExtension:type:objectrequired: -nameproperties:name:description:This field is required, so will never have an optional pointer, nor `omitzero`.type:stringid:description:This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. However, it will receive `omitzero`.type:numberpointer_id:type:numberdescription:This field should have an optional pointer, as the field-level definition of `x-go-type-skip-optional-pointer` overrides the `prefer-skip-optional-pointer` Output Option. This will also not receive an `omitzero`.# NOTE that this overrides the global preferencex-go-type-skip-optional-pointer:falseno_omit:type:numberdescription:This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. This will not receive `omitzero`, as the field-level definition of `x-omitzero` overrides the `prefer-skip-optional-pointer-with-omitzero` Output Option.# NOTE that this overrides the global preferencex-omitzero:false
We then generate the following Go code:
// ...// ClientWithExtension defines model for ClientWithExtension.typeClientWithExtensionstruct {// Id This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. However, it will receive `omitzero`.Idfloat32`json:"id,omitempty,omitzero"`// Name This field is required, so will never have an optional pointer, nor `omitzero`.Namestring`json:"name"`// NoOmit This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. This will not receive `omitzero`, as the field-level definition of `x-omitzero` overrides the `prefer-skip-optional-pointer-with-omitzero` Output Option.NoOmitfloat32`json:"no_omit,omitempty"`// PointerId This field should have an optional pointer, as the field-level definition of `x-go-type-skip-optional-pointer` overrides the `prefer-skip-optional-pointer` Output Option. This will also not receive an `omitzero`.PointerId*float32`json:"pointer_id,omitempty"`}
You can see this in more detail inthe example code forprefer-skip-optional-pointer
andexample code forprefer-skip-optional-pointer-with-omitzero
As ofoapi-codegen
v2.2.0, it is now possible to use theoutput-options
configuration'sname-normalizer
to define the logic for how to convert an OpenAPI name (i.e. an Operation ID or a Schema name) and construct a Go type name.
Example, using default configuration
By default,oapi-codegen
will perform camel-case conversion, so for a spec such as:
openapi:"3.0.0"info:version:1.0.0title:Example code for the `name-normalizer` output optionpaths:/api/pets/{petId}:get:summary:Get pet given identifier.operationId:getHttpPetparameters: -name:petIdin:pathrequired:trueschema:type:stringresponses:'200':description:valid petcontent:application/json:schema:$ref:'#/components/schemas/Pet'components:schemas:Pet:type:objectrequired: -uuid -nameproperties:uuid:type:stringdescription:The pet uuid.name:type:stringdescription:The name of the pet.Error:required: -code -messageproperties:code:type:integerformat:int32description:Error codemessage:type:stringdescription:Error messageOneOf2things:description:"Notice that the `things` is not capitalised"oneOf: -type:objectrequired: -idproperties:id:type:integer -type:objectrequired: -idproperties:id:type:stringformat:uuid
This will produce:
// OneOf2things Notice that the `things` is not capitalisedtypeOneOf2thingsstruct {union json.RawMessage}// Pet defines model for Pet.typePetstruct {// Name The name of the pet.Namestring`json:"name"`// Uuid The pet uuid.Uuidstring`json:"uuid"`}// The interface specification for the client above.typeClientInterfaceinterface {// GetHttpPet requestGetHttpPet(ctx context.Context,petIdstring,reqEditors...RequestEditorFn) (*http.Response,error)}
Example, usingToCamelCaseWithInitialisms
By default,oapi-codegen
will perform camel-case conversion, so for a spec such as:
openapi:"3.0.0"info:version:1.0.0title:Example code for the `name-normalizer` output optionpaths:/api/pets/{petId}:get:summary:Get pet given identifier.operationId:getHttpPetparameters: -name:petIdin:pathrequired:trueschema:type:stringresponses:'200':description:valid petcontent:application/json:schema:$ref:'#/components/schemas/Pet'components:schemas:Pet:type:objectrequired: -uuid -nameproperties:uuid:type:stringdescription:The pet uuid.name:type:stringdescription:The name of the pet.Error:required: -code -messageproperties:code:type:integerformat:int32description:Error codemessage:type:stringdescription:Error messageOneOf2things:description:"Notice that the `things` is not capitalised"oneOf: -type:objectrequired: -idproperties:id:type:integer -type:objectrequired: -idproperties:id:type:stringformat:uuid
This will produce:
// OneOf2things Notice that the `things` is not capitalisedtypeOneOf2thingsstruct {union json.RawMessage}// Pet defines model for Pet.typePetstruct {// Name The name of the pet.Namestring`json:"name"`// UUID The pet uuid.UUIDstring`json:"uuid"`}// The interface specification for the client above.typeClientInterfaceinterface {// GetHTTPPet requestGetHTTPPet(ctx context.Context,petIDstring,reqEditors...RequestEditorFn) (*http.Response,error)}
For more details of what the resulting code looks like, check outthe test cases.
Theexamples directory contains some additional cases which are useful examples for how to useoapi-codegen
, including how you'd take the Petstore API and implement it withoapi-codegen
.
You could also find some cases of how the project can be used by checking out ourinternal test cases which are real-world usages that make up our regression tests.
We love reading posts by the community about how to use the project.
Here are a few we've found around the Web:
- Building a Go RESTful API with design-first OpenAPI contracts
- A Practical Guide to Using oapi-codegen in Golang API Development with the Fiber Framework
- Generating Go server code from OpenAPI 3 definitions
- Go Client Code Generation from Swagger and OpenAPI
- Go oapi-codegen + request validation
- Streamlining Go + Chi Development: Generating Code from an OpenAPI Spec
Got one to add? Please raise a PR!
No, we don't currently.
OpenAPI 3.1 support isawaiting upstream support.
In the meantime, you could followsteps from this blog post touse OpenAPI Overlay to "downgrade" the OpenAPI 3.1 spec to OpenAPI 3.0.
oapi-codegen
supportsanyOf
,allOf
andoneOf
for generated code.
For instance, through the following OpenAPI spec:
openapi:"3.0.0"info:version:1.0.0title:Using complex schemasdescription:An example of `anyOf`, `allOf` and `oneOf`components:schemas:# base typesClient:type:objectrequired: -nameproperties:name:type:stringIdentity:type:objectrequired: -issuerproperties:issuer:type:string# allOf performs a union of all types definedClientWithId:allOf: -$ref:'#/components/schemas/Client' -properties:id:type:integerrequired: -id# allOf performs a union of all types defined, but if there's a duplicate field defined, it'll be overwritten by the last schema# https://github.com/oapi-codegen/oapi-codegen/issues/1569IdentityWithDuplicateField:allOf:# `issuer` will be ignored -$ref:'#/components/schemas/Identity'# `issuer` will be ignored -properties:issuer:type:integer# `issuer` will take precedence -properties:issuer:type:objectproperties:name:type:stringrequired: -name# anyOf results in a type that has an `AsClient`/`MergeClient`/`FromClient` and an `AsIdentity`/`MergeIdentity`/`FromIdentity` method so you can choose which of them you want to retrieveClientAndMaybeIdentity:anyOf: -$ref:'#/components/schemas/Client' -$ref:'#/components/schemas/Identity'# oneOf results in a type that has an `AsClient`/`MergeClient`/`FromClient` and an `AsIdentity`/`MergeIdentity`/`FromIdentity` method so you can choose which of them you want to retrieveClientOrIdentity:oneOf: -$ref:'#/components/schemas/Client' -$ref:'#/components/schemas/Identity'
This results in the following types:
Base types
// Client defines model for Client.typeClientstruct {Namestring`json:"name"`}// Identity defines model for Identity.typeIdentitystruct {Issuerstring`json:"issuer"`}
allOf
// ClientWithId defines model for ClientWithId.typeClientWithIdstruct {Idint`json:"id"`Namestring`json:"name"`}// IdentityWithDuplicateField defines model for IdentityWithDuplicateField.typeIdentityWithDuplicateFieldstruct {Issuerstruct {Namestring`json:"name"`}`json:"issuer"`}
anyOf
import ("encoding/json""github.com/oapi-codegen/runtime")// ClientAndMaybeIdentity defines model for ClientAndMaybeIdentity.typeClientAndMaybeIdentitystruct {union json.RawMessage}// AsClient returns the union data inside the ClientAndMaybeIdentity as a Clientfunc (tClientAndMaybeIdentity)AsClient() (Client,error) {varbodyClienterr:=json.Unmarshal(t.union,&body)returnbody,err}// FromClient overwrites any union data inside the ClientAndMaybeIdentity as the provided Clientfunc (t*ClientAndMaybeIdentity)FromClient(vClient)error {b,err:=json.Marshal(v)t.union=breturnerr}// MergeClient performs a merge with any union data inside the ClientAndMaybeIdentity, using the provided Clientfunc (t*ClientAndMaybeIdentity)MergeClient(vClient)error {b,err:=json.Marshal(v)iferr!=nil {returnerr}merged,err:=runtime.JSONMerge(t.union,b)t.union=mergedreturnerr}// AsIdentity returns the union data inside the ClientAndMaybeIdentity as a Identityfunc (tClientAndMaybeIdentity)AsIdentity() (Identity,error) {varbodyIdentityerr:=json.Unmarshal(t.union,&body)returnbody,err}// FromIdentity overwrites any union data inside the ClientAndMaybeIdentity as the provided Identityfunc (t*ClientAndMaybeIdentity)FromIdentity(vIdentity)error {b,err:=json.Marshal(v)t.union=breturnerr}// MergeIdentity performs a merge with any union data inside the ClientAndMaybeIdentity, using the provided Identityfunc (t*ClientAndMaybeIdentity)MergeIdentity(vIdentity)error {b,err:=json.Marshal(v)iferr!=nil {returnerr}merged,err:=runtime.JSONMerge(t.union,b)t.union=mergedreturnerr}func (tClientAndMaybeIdentity)MarshalJSON() ([]byte,error) {b,err:=t.union.MarshalJSON()returnb,err}func (t*ClientAndMaybeIdentity)UnmarshalJSON(b []byte)error {err:=t.union.UnmarshalJSON(b)returnerr}
oneOf
// AsClient returns the union data inside the ClientOrIdentity as a Clientfunc (tClientOrIdentity)AsClient() (Client,error) {varbodyClienterr:=json.Unmarshal(t.union,&body)returnbody,err}// FromClient overwrites any union data inside the ClientOrIdentity as the provided Clientfunc (t*ClientOrIdentity)FromClient(vClient)error {b,err:=json.Marshal(v)t.union=breturnerr}// MergeClient performs a merge with any union data inside the ClientOrIdentity, using the provided Clientfunc (t*ClientOrIdentity)MergeClient(vClient)error {b,err:=json.Marshal(v)iferr!=nil {returnerr}merged,err:=runtime.JSONMerge(t.union,b)t.union=mergedreturnerr}// AsIdentity returns the union data inside the ClientOrIdentity as a Identityfunc (tClientOrIdentity)AsIdentity() (Identity,error) {varbodyIdentityerr:=json.Unmarshal(t.union,&body)returnbody,err}// FromIdentity overwrites any union data inside the ClientOrIdentity as the provided Identityfunc (t*ClientOrIdentity)FromIdentity(vIdentity)error {b,err:=json.Marshal(v)t.union=breturnerr}// MergeIdentity performs a merge with any union data inside the ClientOrIdentity, using the provided Identityfunc (t*ClientOrIdentity)MergeIdentity(vIdentity)error {b,err:=json.Marshal(v)iferr!=nil {returnerr}merged,err:=runtime.JSONMerge(t.union,b)t.union=mergedreturnerr}func (tClientOrIdentity)MarshalJSON() ([]byte,error) {b,err:=t.union.MarshalJSON()returnb,err}func (t*ClientOrIdentity)UnmarshalJSON(b []byte)error {err:=t.union.UnmarshalJSON(b)returnerr}
For more info, check outthe example code.
By default,oapi-codegen
will generate everything from the specification.
If you'd like to reduce what's generated, you can use one of a few options inthe configuration file to tune the generation of the resulting output:
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.jsonoutput-options:include-tags:[]exclude-tags:[]include-operation-ids:[]exclude-operation-ids:[]exclude-schemas:[]
Checkthe docs for more details of usage.
We recommend doing so, yes, for the following reasons:
- It means it's easier to view the impact of a change - be it due to an upgrade of
oapi-codegen
, or a change to your spec - and has helped catch (possibly) breaking changes in the past more easily - It then allows your codebase to be consumed as a library, as all the files are committed
This means you'll need to have your CI/CD pipeline validate that generated files are all up-to-date, but that's a fairly straightforward piece of work.
We really ask that you don't. Although it intends to be idiomatic Go code, it's not expected to pass all the various linting rules that your project may apply.
Note
We will, on occasion, improve the generated code to fix some linting warnings, such as those fromgo vet
, but this should not be an expected change.
Thekin-openapi project - which we 💜 for providing a great library and set of tooling for interacting with OpenAPI - is a pre-v1 release, which means that they're within their rights to push breaking changes.
This may lead to breakage in your consuming code, and if so, sorry that's happened!
We'll be aware of the issue, and will work to update both the coreoapi-codegen
and the middlewares accordingly.
We're very appreciative ofthe many contributors over the years and the ongoing use of the project 💜
For the most part,oapi-codegen
is maintained in two busy peoples' free time. As noted inCreating a more sustainable model foroapi-codegen
in the future, we're looking to make this a more sustainable project in the future.
Please consider sponsoring us through GitHub Sponsors eitheron the organisation ordirectly for Jamie, which helps work towards us being able to maintain the project long term.
Seethis blog post from Tidelift for more details on how to talk to your company about sponsoring maintainers of (Open Source) projects you depend on.
We are currently sponsored for 4 hours of work a month by Elastic:
In addition, we are also generously sponsored by the following folks, each of whom provide sponsorship for 1 hour of work a month:
(Note that the order of appearance the order in which sponsorship was received)
About
Generate Go client and server boilerplate from OpenAPI 3 specifications
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.