Generating Go code from OpenAPI schemas
Turn OpenAPI schemas into clean, dependency-free Go models.The go generator turns OpenAPI schemas into Go model types. 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.
No runtime package to import, no dependencies. 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 v0.37.
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 will generate:
package models
type Product_Status string
type Product_Dimensions struct {
Width *float64 `json:"width,omitempty"`
Height *float64 `json:"height,omitempty"`
}
// Product A product in the catalog.
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Status *Product_Status `json:"status,omitempty"`
Dimensions *Product_Dimensions `json:"dimensions,omitempty"`
}
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 will output
package catalog
// Product A product in the catalog.
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Category *Category `json:"category,omitempty"`
}
type Category struct {
Name string `json:"name"`
Parent *string `json:"parent,omitempty"`
}
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 shape | Generated Go | Description |
|---|---|---|
object |
struct |
With properties, each property becomes a field on the model type. |
array |
[]T |
The item schema decides the slice element type. |
object |
map[string]T |
With schema additionalProperties, JSON methods preserve extra keys. |
| scalar | string / int / float64 / bool |
string, integer, number, and boolean use these defaults. |
integer |
int32 / int64 |
format: int32 or format: int64 selects the sized integer. |
number |
float32 / float64 |
format: float maps to float32; double maps to float64. |
enum |
named type | Single-type scalar enums can also emit typed constants. |
oneOf |
typed union | A discriminator or shared const generates an interface and wrapper. |
oneOf / anyOf |
json.RawMessage wrapper |
Ambiguous compositions preserve the payload as raw JSON. |
| optional property | pointer field | Properties not in required receive omitempty unless disabled. |
| nullable | pointer | null in the type, or a nullable reference, becomes 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:
package models
import (
"encoding/json"
"fmt"
)
// Payment_Card Card.
type Payment_Card struct {
Kind string `json:"kind"`
Number string `json:"number"`
}
// Payment_Bank Bank.
type Payment_Bank struct {
Kind string `json:"kind"`
Account string `json:"account"`
}
type Payment interface {
isPayment()
}
func (Payment_Card) isPayment() {}
func (Payment_Bank) isPayment() {}
type PaymentUnion struct {
Value Payment
}
func (u PaymentUnion) MarshalJSON() ([]byte, error) {
if u.Value == nil {
return []byte("null"), nil
}
return json.Marshal(u.Value)
}
func (u PaymentUnion) IsZero() bool {
return u.Value == nil
}
func (u *PaymentUnion) UnmarshalJSON(data []byte) error {
var discriminator struct {
Value string `json:"kind"`
}
if err := json.Unmarshal(data, &discriminator); err != nil {
return err
}
switch discriminator.Value {
case "bank":
var v Payment_Bank
if err := json.Unmarshal(data, &v); err != nil {
return err
}
u.Value = v
case "card":
var v Payment_Card
if err := json.Unmarshal(data, &v); err != nil {
return err
}
u.Value = v
default:
return fmt.Errorf("unknown kind discriminator value %q", discriminator.Value)
}
return nil
}
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:
package models
type Status string
const (
StatusDraft Status = "draft"
StatusActive Status = "active"
StatusDiscontinued Status = "discontinued"
)
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:
package models
import "encoding/json"
type Labels struct {
Name *string `json:"name,omitempty"`
AdditionalProperties map[string]string `json:"-"`
}
func (m *Labels) UnmarshalJSON(data []byte) error {
type Alias Labels
var known Alias
if err := json.Unmarshal(data, &known); err != nil {
return err
}
*m = Labels(known)
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
delete(raw, "name")
if len(raw) == 0 {
return nil
}
m.AdditionalProperties = make(map[string]string, len(raw))
for key, value := range raw {
var decoded string
if err := json.Unmarshal(value, &decoded); err != nil {
return err
}
m.AdditionalProperties[key] = decoded
}
return nil
}
func (m Labels) MarshalJSON() ([]byte, error) {
type Alias Labels
encoded, err := json.Marshal(Alias(m))
if err != nil {
return nil, err
}
var object map[string]json.RawMessage
if err := json.Unmarshal(encoded, &object); err != nil {
return nil, err
}
for key, value := range m.AdditionalProperties {
encodedValue, err := json.Marshal(value)
if err != nil {
return nil, err
}
object[key] = encodedValue
}
return json.Marshal(object)
}
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.