Generating OpenAPI schemas from Go types

Reflect Go types straight into OpenAPI schema models.

The go generator runs in both directions. Generating Code turns OpenAPI schemas into Go types. This page covers the return trip..

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 v0.37.


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:

type: object
properties:
    id:
        type: string
    name:
        type: string
    price:
        type: number
        format: double
required:
    - id
    - name
    - price

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:

Category:
type: object
properties:
    name:
        type: string
    parent:
        type: string
required:
    - name

Product:
type: object
properties:
    id:
        type: string
    name:
        type: string
    category:
        $ref: '#/components/schemas/Category'
required:
    - category
    - id
    - name

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:

type: object
properties:
    id:
        type: string
        format: uuid
    price:
        type: number
        minimum: 0
    tier:
        type: string
        enum:
            - gold
            - silver
            - bronze
required:
    - id
    - price
    - tier

The tag understands the common scalar metadata keywords:

Tag key Schema keyword
format same
title, description same
minimum, maximum same
exclusiveMinimum, exclusiveMaximum same
multipleOf same
minLength, maxLength, pattern same
minItems, maxItems, uniqueItems same
minProperties, maxProperties same
enum=a|b|c enum (values separated by |)
const same
nullable native nullability
readOnly, writeOnly, deprecated same

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:

Method Interface Returns
OpenAPISchema() *base.SchemaProxy SchemaProvider a libopenapi schema proxy, built directly
OpenAPISchemaYAML() string SchemaYAMLProvider the schema as a YAML string
OpenAPISchemaMetadata() any SchemaMetadataProvider 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:

type: string
pattern: '^[0-9]+ [A-Z]{3}$'
example: 100 USD

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:

Animal:
oneOf:
    - $ref: '#/components/schemas/Cat'
    - $ref: '#/components/schemas/Dog'
discriminator:
    propertyName: kind
    mapping:
        cat: '#/components/schemas/Cat'
        dog: '#/components/schemas/Dog'

Cat:
type: object
properties:
    kind:
        type: string
    name:
        type: string
required:
    - kind
    - name

Dog:
type: object
properties:
    kind:
        type: string
    name:
        type: string
required:
    - kind
    - name

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:

type: object
properties:
    name:
        type: string
    description:
        type:
            - string
            - "null"
required:
    - name

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.