Skip to content

About

Generate idiomatic Go code from an OpenAPI specification: types, a typed HTTP client, an http.Handler server scaffold, and tests. Flattens and compresses the spec first, so that generated types are named and free of duplicates.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

290 Commits

Folders and files

Repository files navigation

Go Reference Code Coverage License

A gopher cranking a machine that turns a rolled-up scroll into a stack of printed pages

From API spec to Go code you'd have written yourself.

Code Coverage

openapi-codegen parses an OpenAPI 3.x specification and generates idiomatic Go code — types, an HTTP client, an HTTP server scaffold, and tests.

Introduction

Generated code has a reputation for being obviously generated. This module tries hard not to earn it: output is run through goimports and gofumpt, names are converted to Go conventions rather than transliterated, and the emitted types are the ones you would have declared by hand.

Getting there depends on the specification being in good shape first, which is why this module doesn't work from the raw document. It normalizes the spec through the rest of the family before generating anything:

  1. Load the specification, and any recorded HTTP interactions
  2. Validate it
  3. Flatten — every meaningful type gets a name, so it can become a named Go type
  4. Compress — duplicate schemas collapse, so the same shape doesn't become five Go types
  5. Build an intermediate representation, resolving schemas to Go types
  6. Match recorded interactions to operations, for round-trip tests
  7. Render and format the output

Features

  • types — structs, enums, and type aliases for all referenced schemas
  • client — typed HTTP client with per-operation methods
  • server — http.Handler-based server scaffold
  • tests — round-trip and cassette-backed tests, generated from recorded traffic
  • JavaScript client — optional api.js alongside the Go output

Identifiers are sanitized into valid, idiomatic Go: leading digits are spelled out, punctuation is stripped, acronyms are preserved, and names that would collide with the error interface or a Go keyword are renamed.

How the specification maps onto Go:

  • Unions — a oneOf or anyOf becomes a struct with one field per alternative, exactly one (oneOf) or at least one (anyOf) of them set after decoding. A field is a pointer, nil until set, unless its zero value already says it is not set: nil for a slice or a map, "" for a string. An alternative that is only null needs no field. Where a member tells the alternatives apart (the discriminator's propertyName, or a member each alternative fixes to a string of its own, such as Notion's type), that member names the alternative. When it comes first, the alternative it names decodes each further member as it is read, without reading the whole value first; when it does not, the value is read whole and then decoded the same way. An unknown value, or a value without that member, is an error. An alternative that is a union of its own counts by its alternatives, however deep, so its leaves are chosen the same way. Encoding writes the discriminator first, with the value of the alternative set, and refuses a different one. Otherwise each alternative is tried in turn, and only where the value has the members it requires and the values it pins (const or a one-value enum), which decoding a struct alone does not check, strict or lenient.
  • Tagged unions — a union whose alternatives differ only in a tag, a member each fixes to a string of its own, and in members of their own, such as Notion's blocks ({"type": "paragraph", "paragraph": {...}}), becomes one struct instead: the members all alternatives share, the tag, and one optional field per member of an alternative's own. An alternative's own members are at most one, named after its value, or more beside that one, such as a relation's has_more; one that several alternatives have must be the same in each. The tag is an enum type of its values, named after the struct and the tag (such as BlockType with BlockTypeChildDatabase), unless a part of an allOf types it already or a name is taken. The methods check that only members of the alternative the tag names are set, and that those it requires are there, as null if need be: decoding checks the members the object holds, encoding the fields set, where a member that may be null cannot be told from one left out. Encoding with the tag left empty sends the value whose member is set, and where an alternative may leave the tag out, decoding infers it so. Alternatives that are unions tagged alike count by their own alternatives, and as part of an allOf the union's fields join the struct's. A tagged struct that is an alternative of another union, there or in an allOf, is chosen by the members present, as an object is. Alternatives nothing else refers to get no type of their own.
  • allOf — each part referenced by this schema alone is folded into its fields; a part other schemas share stays an embedded type. Properties the schema declares beside its allOf are fields too, after the parts'. A union among the parts is a field of its own, decoded by the struct's methods: the fields and the chosen alternative each take the members they declare, chosen by the discriminator or by which members are present, and a member none of them declares is an error.
  • Strictness — decoding fails as soon as the input differs from what the specification allows, so that an incomplete specification shows itself. The generated methods report it as encoding/json does, with a *json.SemanticError locating it in the input; an unknown member wraps json.ErrUnknownName. A case that could be supported but has no real example yet, such as an allOf of two unions, is generated with methods that return an "unimplemented" error.
  • Debug mode — with -debug, a client given WithDebug records each response it fails to decode to api/interactions.json, for openapi-enrich to learn from, then decodes it again without rejecting members the specification does not know. Only if that fails too does the call fail, so a response that merely holds more than the specification says still reaches the caller. Nothing the specification leaves open decodes into any then: the empty schema, an array without items, a free-form object and not alone become struct{}, so any value in them is recorded, and any but an object fails.
  • Fields — a field is a pointer only where its zero value must be told apart from something else: from leaving the field out, if it is optional, or from null, if it is nullable. That is a boolean, a number that may be 0, or an object that requires nothing, so {} says something; nil then leaves the field out, and a pointer to the zero value sends it. A zero value the specification makes the default, or rules out with a bound or an enum, needs no pointer. A string, a time, a slice, a map or a union is never a pointer: its zero value leaves it out. A struct that would contain itself refers to itself through a pointer.
  • Omitting — an optional field is tagged omitzero, so it is left out while it holds its zero value: nil for a pointer, a slice or a map, so an empty one is still sent. A required field is always sent, "", 0 and false included.
  • Read-only and write-only — a property only responses carry (readOnly) or only requests (writeOnly) is never required, so a value that leaves it out still decodes, and a union's alternative is chosen without it. The client leaves read-only members out of a request body, and the server leaves write-only members out of a response, as their fields hold them or not, through unions and embedded parts alike; any other encoding keeps them.
  • Null — a schema that is only ever null is *struct{}, and "X or null" is X, a pointer to X only by the rule for fields above, so that null and the zero value can differ, or where X decodes itself, such as a union or a tuple, which would refuse null.
  • Fixed parameters — a required parameter that can take only one value, its const or the only value of its enum, is sent by the client itself: in the path, the query or the headers, such as Notion's Notion-Version. The caller never passes it. An example alone does not fix a value.
  • Query parameters — an array is sent as one value per element (form style, exploded). "X or an array of X" is sent as the array, a union of strings as a string, and null is dropped, since a query string cannot carry it.
  • Authentication — each operation sends the credential its own security names, else the document's: a bearer token or basic auth, read from the environment. Where a document uses both, NewClient requires at least one, and each operation fails before sending if its own is missing. An operation whose credentials are optional ({} beside a scheme) sends them all the same, and its replay test accepts a recording made without them.
  • Success responses — an operation returns its success body as *T, or as T where T is already nilable: a slice, a map, or a named type of either. An operation whose success body is an empty object returns just error, and the server writes {}. Its body is read only in debug mode, where it is decoded so that anything in it fails loudly and is recorded. XWithResult[R] decodes into a type of the caller's own instead, leniently, since it declares only what it needs; the operation's own type is decoded strictly.
  • Binary responses — a success body that is not text, such as a zip, a PDF, an image or a video, is returned as an io.ReadCloser that reads it as it arrives, never held whole in memory; the caller closes it. A text body that is not JSON is returned as []byte. The server copies a returned reader into the response, and the JavaScript client returns such a body as a Blob, or text as a string.
  • Error responses — an error body's type is returned wrapped in api.Error, so it needs an Error() string method. The generator does not write one, since a good message depends on the API: add it by hand beside the generated code.
  • Names — a component's Go name is its x-go-name, or its key with any character a Go identifier cannot hold removed (Keypoint-Input → KeypointInput). Every type is exported: a key that does not begin with an upper-case letter is renamed in Go style first (idRequest → IDRequest, error_api_400 → ErrorAPI400), with every reference to it. A name another component already holds is numbered (date beside Date → Date2); set x-go-name to choose a better one. A component that is only a $ref, only null or the empty schema declares no type; references to it use what it stands for.

Usage

go get -tool github.com/MarkRosemaker/openapi-codegen/cmd/openapi-codegen

or

go get github.com/MarkRosemaker/openapi-codegen
openapi-codegen -spec openapi.json -out ./gen -pkg mypkg -client

At least one of -client, -server, or -js is required. Types are generated automatically whenever a client or server is, and client tests whenever a client is.

Flag Default Purpose
-spec api/openapi.json Path to the OpenAPI specification
-out pkg/<package> Output directory for generated files
-pkg directory name Go package name
-agent — User-Agent string for the generated client
-client false Generate client.gen.go and client.gen_test.go
-server false Generate server.gen.go
-js false Generate api.js

It can also be used as a library:

import "github.com/MarkRosemaker/openapi-codegen"

err := codegen.Generate(codegen.Config{
    SpecPath:    "api/openapi.json",
    OutputDir:   "pkg/mypkg",
    PackageName: "mypkg",
})

Config accepts an already-parsed *openapi.Document instead of SpecPath, and an afero.Fs instead of OutputDir, so generation can run entirely in memory.

The openapi family

Module Purpose
openapi Parse, validate, and write OpenAPI 3.x specifications
openapi-compare Compare specification objects — exact equality and shape equivalence
openapi-edit Safe structural edits, such as renaming a schema and rewriting every $ref to it
openapi-flatten Promote inline definitions into named components entries
openapi-compress Deduplicate and merge equivalent component schemas
openapi-merge Merge schemas that were inferred independently from different samples
openapi-enrich Infer specification content from observed HTTP traffic
openapi-codegen (this module) Generate Go types, clients, and servers from a specification

This module sits at the end of the pipeline. If you have no specification to start from, openapi-enrich can build one from recorded traffic — and the same recordings then become the generated client's tests.

Additional Information

Contributing

Contributions are welcome — please open an issue or a pull request on GitHub.

License

This project is licensed under the Apache 2.0 License.

About

Generate idiomatic Go code from an OpenAPI specification: types, a typed HTTP client, an http.Handler server scaffold, and tests. Flattens and compresses the spec first, so that generated types are named and free of duplicates.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages