Generating Go code from OpenAPI schemas
Turn OpenAPI schemas into clean, dependency-free Go models.The golang generator turns OpenAPI schemas into Go model types. You hand it libopenapi schema
models and it returns gofmt’d Go source, along with a set of diagnostics that describe any schema
shape that didn’t map cleanly onto a plain Go field.
It’s library-only. There is no CLI, no generated client or server, and no runtime package to import. It generates source code, and nothing else. The generated models depend only on the Go standard library.
The generator is part of the core libopenapi module, so there’s no separate dependency to add. Import
github.com/pb33f/libopenapi/generator/golang and you’re ready to go.
Available since libopenapi vX.Y.Z.
Rendering a single schema
RenderSchema takes a name and a *base.SchemaProxy, and returns formatted Go source. Here we parse
a small specification, pull the Product schema out of components.schemas, and render it:
package main
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/generator/golang"
)
const spec = `openapi: 3.1.0
info:
title: Catalog
version: 1.0.0
paths: {}
components:
schemas:
Product:
type: object
description: A product in the catalog.
required: [id, name, price]
properties:
id:
type: string
format: uuid
name:
type: string
price:
type: number
format: double
status:
type: string
enum: [draft, active, discontinued]
dimensions:
type: object
properties:
width:
type: number
height:
type: number
`
func main() {
// parse an OpenAPI document from bytes
doc, err := libopenapi.NewDocument([]byte(spec))
if err != nil {
panic(err)
}
// build the v3 model so the component schemas are available
model, errs := doc.BuildV3Model()
if errs != nil {
panic(errs)
}
// pull the Product schema out of components.schemas
product, _ := model.Model.Components.Schemas.Get("Product")
// render it as Go source
source, err := golang.RenderSchema("Product", product)
if err != nil {
panic(err)
}
fmt.Print(string(source))
}
This prints:
Inline objects (dimensions) and enums (status) are lifted into their own named types, using _
as the delimiter between parent and child. Required fields are values; optional fields are pointers
with omitempty. The schema description becomes a doc comment.
Rendering a whole component map
RenderSchemas takes the entire components.schemas map and returns a single *GeneratedFile. A
$ref between two components becomes a typed field that points at the generated type:
package main
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/generator/golang"
)
const spec = `openapi: 3.1.0
info:
title: Catalog
version: 1.0.0
paths: {}
components:
schemas:
Product:
type: object
description: A product in the catalog.
required: [id, name, price]
properties:
id:
type: string
format: uuid
name:
type: string
price:
type: number
format: double
category:
$ref: '#/components/schemas/Category'
Category:
type: object
required: [name]
properties:
name:
type: string
parent:
type: string
`
func main() {
doc, err := libopenapi.NewDocument([]byte(spec))
if err != nil {
panic(err)
}
model, errs := doc.BuildV3Model()
if errs != nil {
panic(errs)
}
// render every schema in components.schemas into one file
gen := golang.NewGenerator(golang.WithPackageName("catalog"))
file, err := gen.RenderSchemas(model.Model.Components.Schemas)
if err != nil {
panic(err)
}
fmt.Printf("package name: %s\n", file.PackageName)
fmt.Print("types: ")
for i, t := range file.Types {
if i > 0 {
fmt.Print(", ")
}
fmt.Print(t.Name)
}
fmt.Printf("\ndiagnostics: %d\n", len(file.Diagnostics))
fmt.Print("---\n")
fmt.Print(string(file.Source))
}
This prints:
What you get back
RenderSchemas returns a *GeneratedFile. RenderSchema is a shortcut that returns just the
Source bytes.
| Field | Description |
|---|---|
PackageName |
The package name written at the top of the file. |
Source |
The gofmt’d Go source, as bytes. |
Types |
One entry per top-level generated type, each with a Name and a Kind. |
Diagnostics |
Notable or lossy decisions made while generating (see below). |
SchemaMetadata |
Optional sidecar source for high-fidelity round trips. nil unless enabled. |
How OpenAPI shapes map to Go
| OpenAPI schema | Generated Go |
|---|---|
object with properties |
a struct |
array |
a slice, []T |
object with a schema additionalProperties |
map[string]T plus marshal/unmarshal methods |
string / integer / number / boolean |
string / int / float64 / bool |
integer with format: int32 / int64 |
int32 / int64 |
number with format: float / double |
float32 / float64 |
enum of a single scalar type |
a named type, plus typed constants when enabled |
oneOf with a discriminator (or shared const) |
a typed union: an interface and a …Union wrapper |
ambiguous oneOf / anyOf |
a json.RawMessage wrapper |
a property not in required |
a pointer field with omitempty |
null in the type, or a nullable reference |
a pointer |
A string format stays a string unless you map it yourself with WithFormatMapping (covered
below). Validation keywords such as minimum or pattern describe constraints, not shape, so they
don’t change the generated Go. The generator records a diagnostic when it sees them.
Diagnostics
The generator never silently drops information. Anything it can’t represent as a plain Go field, or
any decision worth knowing about, is reported as a Diagnostic with a Code, a Path, and a
Message:
package main
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/generator/golang"
)
const spec = `openapi: 3.1.0
info:
title: Catalog
version: 1.0.0
paths: {}
components:
schemas:
Product:
type: object
required: [price]
properties:
price:
type: number
minimum: 0
tags:
type: array
items:
type: string
uniqueItems: true
`
func main() {
doc, err := libopenapi.NewDocument([]byte(spec))
if err != nil {
panic(err)
}
model, errs := doc.BuildV3Model()
if errs != nil {
panic(errs)
}
file, err := golang.NewGenerator().RenderSchemas(model.Model.Components.Schemas)
if err != nil {
panic(err)
}
for _, d := range file.Diagnostics {
fmt.Printf("[%s] %s: %s\n", d.Code, d.Path, d.Message)
}
}
This prints:
Shaping the output
The default settings produce idiomatic models, but every decision is configurable through options
passed to RenderSchema, RenderSchemas, or NewGenerator.
| Option | Effect |
|---|---|
WithPackageName(name) |
Set the package name. Defaults to models. |
WithGeneratedComment(true) |
Add the // Code generated … DO NOT EDIT. header. |
WithOptionalFieldsAsPointers(false) |
Render optional fields as values instead of pointers. |
WithOmitEmpty(false) |
Drop omitempty from optional tags. |
WithGenerateYAMLTags(true) |
Add yaml tags alongside the json tags. |
WithEnumConstants(true) |
Emit a typed constant for each enum value. |
WithOptionalConstDiscriminatorUnions(true) |
Let an optional shared const property still form a typed union. |
WithAdditionalPropertiesMethods(false) |
Skip the generated marshal/unmarshal methods for additionalProperties. |
WithNestedTypeNameDelimiter("") |
Change the parent/child delimiter. Defaults to _. |
WithFormatMapping(format, goType, import) |
Map an OpenAPI string format to a Go type and import path. |
WithTypeNameResolver(fn) |
Supply your own naming for generated types (also fields, enums, and refs). |
WithSchemaMetadataSidecar(true) |
Emit a metadata sidecar for high-fidelity round trips. |
Typed unions
A oneOf whose variants share a required property with a distinct const value becomes a typed
union. The generator emits an interface, one concrete type per variant, and a …Union wrapper whose
UnmarshalJSON switches on the discriminator:
package main
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/generator/golang"
)
const spec = `openapi: 3.1.0
info:
title: Catalog
version: 1.0.0
paths: {}
components:
schemas:
Payment:
oneOf:
- title: Card
type: object
required: [kind, number]
properties:
kind:
type: string
const: card
number:
type: string
- title: Bank
type: object
required: [kind, account]
properties:
kind:
type: string
const: bank
account:
type: string
`
func main() {
doc, err := libopenapi.NewDocument([]byte(spec))
if err != nil {
panic(err)
}
model, errs := doc.BuildV3Model()
if errs != nil {
panic(errs)
}
payment, _ := model.Model.Components.Schemas.Get("Payment")
source, err := golang.RenderSchema("Payment", payment)
if err != nil {
panic(err)
}
fmt.Print(string(source))
}
This prints:
An explicit discriminator in the schema works the same way. When a oneOf or anyOf is ambiguous
(no discriminator and no shared const), the generator renders a json.RawMessage wrapper instead,
so the model stays valid and dependency-free without guessing at the shape.
Enums as constants
WithEnumConstants(true) emits a typed constant for each enum value:
package main
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/generator/golang"
)
const spec = `openapi: 3.1.0
info:
title: Catalog
version: 1.0.0
paths: {}
components:
schemas:
Status:
type: string
enum: [draft, active, discontinued]
`
func main() {
doc, err := libopenapi.NewDocument([]byte(spec))
if err != nil {
panic(err)
}
model, errs := doc.BuildV3Model()
if errs != nil {
panic(errs)
}
status, _ := model.Model.Components.Schemas.Get("Status")
source, err := golang.RenderSchema("Status", status, golang.WithEnumConstants(true))
if err != nil {
panic(err)
}
fmt.Print(string(source))
}
This prints:
Round-tripping unknown fields
When a schema has a schema-valued additionalProperties, the generated model keeps the known fields
as struct fields and collects everything else into an AdditionalProperties map. The generator also
emits MarshalJSON and UnmarshalJSON methods so unknown fields survive a decode/encode cycle:
package main
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/generator/golang"
)
const spec = `openapi: 3.1.0
info:
title: Catalog
version: 1.0.0
paths: {}
components:
schemas:
Labels:
type: object
properties:
name:
type: string
additionalProperties:
type: string
`
func main() {
doc, err := libopenapi.NewDocument([]byte(spec))
if err != nil {
panic(err)
}
model, errs := doc.BuildV3Model()
if errs != nil {
panic(errs)
}
labels, _ := model.Model.Components.Schemas.Get("Labels")
source, err := golang.RenderSchema("Labels", labels)
if err != nil {
panic(err)
}
fmt.Print(string(source))
}
This prints:
Pass WithAdditionalPropertiesMethods(false) if you’d rather provide the JSON behavior yourself.
Going the other way
The generator also runs in reverse: hand it Go types and it produces OpenAPI schemas. See Parsing Code for the return trip, and for the metadata sidecar that makes an OpenAPI → Go → OpenAPI round trip lossless.