openapi-flatten eliminates nesting in OpenAPI 3.x
specifications. It promotes inline schema definitions, responses, request bodies,
and parameters into the top-level components section and replaces them with $ref
references.
OpenAPI allows schemas to be defined inline anywhere they are used. While convenient for small specs, deeply nested inline definitions make large specs harder to read, harder to reuse, and harder to generate consistent client code from. Flattening gives every meaningful type a name and a single canonical location.
This matters most upstream of code generation: a generator can only emit a named,
reusable Go type for a schema that has a name. Flattening first means
openapi-codegen never has to
invent one.
- Promotes inline schemas to
components/schemas, leaving simple scalars in place - Promotes responses, request bodies, and parameters to their respective
componentssections - Generates readable PascalCase names with automatic collision avoidance, and,
if asked, marks each with where it came from (
x-flattened-from) - Hoists shared parameters common to every operation on a path up to the path item
- Normalizes a common path prefix (such as
/v1) into the server URLs - Reports errors with the full JSON path to the offending field
go get -tool github.com/MarkRosemaker/openapi-flatten/cmd/openapi-flattenor
go get github.com/MarkRosemaker/openapi-flattenimport (
"github.com/MarkRosemaker/openapi"
flatten "github.com/MarkRosemaker/openapi-flatten"
)
// Load an OpenAPI document (JSON or YAML)
doc, err := openapi.LoadFromDataJSON(jsonBytes)
if err != nil {
log.Fatal(err)
}
// Flatten all inline definitions
if err := flatten.Document(doc, flatten.Config{}); err != nil {
log.Fatal(err)
}
// doc now has no nested inline objects — only $ref pointersDocument is the entire public API. It modifies the document in place.
With Config{MarkOrigin: true}, each schema Document moves into
components/schemas gets an x-flattened-from extension: a JSON pointer to the
$ref left in its place, such as #/components/schemas/Page/properties/cover.
A schema without it was a component already, so a later step can tell a name the
specification gave from one flatten made up. openapi-compress reads it, to
prefer the specification's names when it merges schemas, and removes it. The
command line tool takes -mark-origin for it.
Inline schemas are moved to components/schemas when they contain meaningful structure. Simple scalar types (integer, number, boolean, plain string) stay inline to keep the spec readable. A schema is moved when it is:
- an object with properties
- a string or array of strings with
enumvalues - an array of objects
The entries of an allOf and the alternatives of a oneOf or anyOf are moved by
the same rules, so each shaped one gets a name of its own, and so are the schema of
an object's keys (propertyNames) and the schema a value must not match (not). An
allOf thus ends up as a list of references, plus any entry with no shape of its own,
such as {"required": ["id"]}.
Before:
{
"paths": {
"/pets": {
"post": {
"operationId": "createPet",
"responses": {
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
}
}
}
}
}
}
}
}
}After:
{
"paths": {
"/pets": {
"post": {
"operationId": "createPet",
"responses": {
"400": {
"$ref": "#/components/responses/CreatePetBadRequest"
}
}
}
}
},
"components": {
"responses": {
"CreatePetBadRequest": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePetBadRequest"
}
}
}
}
},
"schemas": {
"CreatePetBadRequest": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
}
}
}
}Every inline response object is moved to components/responses. The generated name combines the operation ID and the HTTP status text:
{OperationID}{StatusText}
Examples: CreatePetBadRequest, GetMeUnauthorized.
The schema of a response's content takes the same name in components/schemas, unless
it has a title. The two sections keep them apart, so neither needs a suffix. Every
response other than a success (2XX) always has its schema promoted to components;
a success response only promotes a complex schema.
Inline request bodies are moved to components/requestBodies, named after the
operation ID alone:
{OperationID}
Example: CreatePet. Its schema takes the same name in components/schemas, unless it
has a title.
Inline parameters are moved to components/parameters using the parameter's own name field.
Once flattened, parameters that appear on every operation of a path are hoisted up
to that path item's shared parameters list, and the per-operation copies removed.
If every path in the document begins with the same segment — a version prefix such
as /v1, for example — that segment is stripped from the path keys and appended to
each server URL instead. The effective URLs are unchanged; the paths just stop
repeating themselves.
All names are converted to Go-style PascalCase (e.g., create pet bad request → CreatePetBadRequest). If the generated name is already taken, a numeric suffix is appended (Name2, Name3, …) to avoid collisions.
An allOf entry is named by its parent, the keyword and its position (PetAllOf1). A oneOf or anyOf alternative is named by its title if it has one ("A person" → APerson), and otherwise the same way (PetOneOf1). The schema of an object's keys is named after the object plus Key (PetKey), and a not schema plus Not (PetNot).
Errors include the full JSON path to the offending field, powered by errpath:
paths["/pets"].post.responses["400"]["application/json"].schema: unimplemented schema ref type "null"
| 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 (this module) | 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 | Generate Go types, clients, and servers from a specification |
Flattening naturally produces near-duplicate components, since the same shape
promoted from two places gets two names. Running
openapi-compress afterwards
collapses them again — the two are designed to be used in that order.
- Go Reference: API documentation.
Contributions are welcome — please open an issue or a pull request on GitHub.
This project is licensed under the Apache 2.0 License.
