Generating OpenAPI schemas from Go types
Reflect Go types straight into OpenAPI schema models.The golang generator runs in both directions. Generating Code
turns OpenAPI schemas into Go types. This page covers the return trip: you hand it Go types, and it
reflects them into libopenapi schema models you can render to YAML or JSON, or drop straight into a
document you’re building.
This is the path for code-first API design. Your Go types are the source of truth, and the schema is generated from them.
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.
A schema from a single type
SchemaFromType takes a reflect.Type and returns a *base.SchemaProxy. Call Render() on it to
get YAML:
package main
import (
"fmt"
"reflect"
"github.com/pb33f/libopenapi/generator/golang"
)
// Product is a plain Go struct.
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func main() {
// reflect the Go type into an OpenAPI schema
proxy, err := golang.SchemaFromType(reflect.TypeOf(Product{}))
if err != nil {
panic(err)
}
// render the schema to YAML
out, err := proxy.Render()
if err != nil {
panic(err)
}
fmt.Print(string(out))
}
This prints:
The field names come from the json tags. Every field is required, because none of them are marked
omitempty. A float64 becomes number with format: double. If you have a value rather than a
type, SchemaFromValue does the same thing from reflect.TypeOf(value).
A component graph from many types
SchemasFromTypes walks one or more types and returns a *SchemaSet. Named struct types become
reusable components, and a field of a named type becomes a $ref to it:
package main
import (
"fmt"
"reflect"
"github.com/pb33f/libopenapi/generator/golang"
)
type Category struct {
Name string `json:"name"`
Parent string `json:"parent,omitempty"`
}
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Category Category `json:"category"`
}
func main() {
// reflect both types into a component graph
set, err := golang.SchemasFromTypes(
reflect.TypeOf(Product{}),
reflect.TypeOf(Category{}),
)
if err != nil {
panic(err)
}
// Components holds every named schema discovered while walking the types
for name, proxy := range set.Components.FromOldest() {
out, _ := proxy.Render()
fmt.Printf("%s:\n%s\n", name, string(out))
}
}
This prints:
Product.category is rendered as a $ref to the Category component rather than being inlined. The
SchemaSet gives you everything you need to assemble a document:
| Field | Description |
|---|---|
Root |
The first requested root schema, for single-root callers. |
Roots |
Every requested root schema, keyed by generated type name. |
Components |
Every reusable schema discovered while walking the type graph. |
Diagnostics |
Notable decisions made while reflecting. |
Adding metadata with openapi tags
Go reflection only sees the Go type. It can’t see a format, a numeric bound, or an enum, because
those don’t exist in Go. The openapi struct tag fills that gap. Each tag is a ;-separated list of
key=value pairs:
package main
import (
"fmt"
"reflect"
"github.com/pb33f/libopenapi/generator/golang"
)
// Product carries openapi struct tags that add metadata Go types can't express.
type Product struct {
ID string `json:"id" openapi:"format=uuid"`
Price float64 `json:"price" openapi:"minimum=0"`
Tier string `json:"tier" openapi:"enum=gold|silver|bronze"`
}
func main() {
proxy, err := golang.SchemaFromType(reflect.TypeOf(Product{}))
if err != nil {
panic(err)
}
out, err := proxy.Render()
if err != nil {
panic(err)
}
fmt.Print(string(out))
}
This prints:
The tag understands the common scalar metadata keywords:
| Tag key | Schema keyword |
|---|---|
format |
format |
title, description |
title, description |
minimum, maximum |
minimum, maximum |
exclusiveMinimum, exclusiveMaximum |
exclusiveMinimum, exclusiveMaximum |
multipleOf |
multipleOf |
minLength, maxLength, pattern |
minLength, maxLength, pattern |
minItems, maxItems, uniqueItems |
minItems, maxItems, uniqueItems |
minProperties, maxProperties |
minProperties, maxProperties |
enum=a|b|c |
enum (values separated by |) |
const |
const |
nullable |
native nullability |
readOnly, writeOnly, deprecated |
readOnly, writeOnly, deprecated |
Exact schemas with provider methods
When a tag isn’t enough, a type can supply its own schema by implementing one of three interfaces. The generator checks for these before it reflects the type:
| Interface | Method | Returns |
|---|---|---|
SchemaProvider |
OpenAPISchema() *base.SchemaProxy |
a libopenapi schema proxy, built directly |
SchemaYAMLProvider |
OpenAPISchemaYAML() string |
the schema as a YAML string |
SchemaMetadataProvider |
OpenAPISchemaMetadata() any |
typed metadata (used by the sidecar) |
SchemaYAMLProvider is the simplest. Here a Money type that Go would otherwise render as a struct
declares itself a formatted string instead:
package main
import (
"fmt"
"reflect"
"github.com/pb33f/libopenapi/generator/golang"
)
// Money provides its own exact OpenAPI schema as YAML.
type Money struct {
Amount int64
Currency string
}
func (Money) OpenAPISchemaYAML() string {
return "type: string\npattern: '^[0-9]+ [A-Z]{3}$'\nexample: 100 USD"
}
func main() {
proxy, err := golang.SchemaFromType(reflect.TypeOf(Money{}))
if err != nil {
panic(err)
}
out, err := proxy.Render()
if err != nil {
panic(err)
}
fmt.Print(string(out))
}
This prints:
If you can’t add methods to a type, the WithTypeSchema, WithFieldSchema, and
WithFieldSchemaByJSONName options do the same thing from the outside, mapping a specific type or
field to an exact schema while the rest of the model is reflected normally.
Interface unions
A Go interface with several concrete implementations maps onto a oneOf. Register the variants with
WithOneOfTypes, and optionally describe the discriminator with WithDiscriminatorMapping:
package main
import (
"fmt"
"reflect"
"github.com/pb33f/libopenapi/generator/golang"
)
// Animal is a Go interface with two concrete implementations.
type Animal interface {
isAnimal()
}
type Cat struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
type Dog struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
func (Cat) isAnimal() {}
func (Dog) isAnimal() {}
func main() {
// register the concrete variants for the interface, then reflect it
set, err := golang.SchemasFromTypesWithOptions(
[]reflect.Type{reflect.TypeOf((*Animal)(nil)).Elem()},
golang.WithOneOfTypes((*Animal)(nil), Cat{}, Dog{}),
golang.WithDiscriminatorMapping((*Animal)(nil), "kind", map[string]string{
"cat": "#/components/schemas/Cat",
"dog": "#/components/schemas/Dog",
}),
)
if err != nil {
panic(err)
}
for name, proxy := range set.Components.FromOldest() {
out, _ := proxy.Render()
fmt.Printf("%s:\n%s\n", name, string(out))
}
}
This prints:
Nullability
A pointer field is nullable. Reflected nullable values use JSON Schema 2020-12 native nullability
rather than the OpenAPI 3.0 nullable: true keyword. A direct schema gets "null" added to its type
array; a nullable reference is wrapped in an anyOf with a null branch:
package main
import (
"fmt"
"reflect"
"github.com/pb33f/libopenapi/generator/golang"
)
type Product struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
}
func main() {
proxy, err := golang.SchemaFromType(reflect.TypeOf(Product{}))
if err != nil {
panic(err)
}
out, err := proxy.Render()
if err != nil {
panic(err)
}
fmt.Print(string(out))
}
This prints:
Lossless round trips
OpenAPI → Go → OpenAPI is lossy by default. Reflection only recovers what the Go type carries, so validation keywords, examples, and other metadata are dropped on the way back.
Two options on the generating side close that gap.
WithOpenAPITags writes the compact openapi tags shown above onto the generated fields, and
WithSchemaMetadataSidecar emits a separate source file that carries the full original schema for
each type. With both enabled, the reflected schemas match the originals. We test exactly this round
trip against the train-travel specification on every build.
Going the other way
To turn OpenAPI schemas into Go types, see Generating Code.