edit

package module
v0.0.0-...-54bae15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: Apache-2.0 Imports: 10 Imported by: 4

README

Go Reference Code Coverage License

A gopher moving one luggage tag while others, connected by strings, swing into alignment

Change an API spec without breaking it.

openapi-edit provides safe structural edits to an OpenAPI 3.x specification — the kind of change where touching one place obliges you to touch several others, and forgetting one leaves a document that no longer resolves.

Status: early. The scope below is settled and operations arrive one at a time, as each earns its place. RenameSchema, RedirectSchema and ExtractSchema are the first.

Introduction

Renaming a schema is the canonical example. The rename itself is a single map operation, but every $ref that pointed at the old name is now dangling — and those $refs can be anywhere: nested inside another schema's properties, inside an allOf branch, in a response's content, in a parameter, in a callback. Getting this right means walking the entire document. Getting it wrong means a spec that looks fine and fails to resolve.

That traversal is worth writing once, carefully, and reusing.

This module serves two kinds of caller:

  • Directly, when you are writing code against your own specification and want to make a specific change safely, without reimplementing the bookkeeping.
  • As a dependency, for tools like openapi-compress and openapi-flatten that run an algorithm over a whole specification and need the same primitives underneath.

Usage

go get github.com/MarkRosemaker/openapi-edit
import (
    "github.com/MarkRosemaker/openapi"
    edit "github.com/MarkRosemaker/openapi-edit"
)

// Renames the schema and rewrites every reference to it.
if err := edit.RenameSchema(doc, "GetV1PetByPetIDOkJSONResponse", "Pet"); err != nil {
    log.Fatal(err)
}

The schema keeps its position among the components, so a rename produces a one-line change rather than reordering the section.

Renaming a schema to its current name does nothing and reports no error. Otherwise the rename fails, changing nothing at all, in three cases:

Error When
ErrSchemaNotFound components.schemas has no schema under the old name
ErrSchemaExists the new name is already taken by another schema
ErrInvalidSchemaName the new name is not a valid key under components

The second is the interesting one. Renaming onto an existing schema would silently discard one of two different definitions and repoint every reference at whichever survived — a change that looks successful and quietly alters the API.

The third matters more than validity alone suggests: a name containing / would produce a reference that resolves somewhere else entirely, and one containing a space would produce a reference that does not resolve at all. Component keys must match ^[a-zA-Z0-9.\-_]+$.

Extracting inline schemas

ExtractSchema names a schema a document spells out inline wherever it is used. It moves the schemas a function accepts into components.schemas under one name, and replaces each with a reference to it:

// Every inline array of RichText becomes a reference to RichTexts.
err := edit.ExtractSchema(doc, "RichTexts", func(s *openapi.Schema) bool {
    return s.Type == openapi.TypeArray && s.Items != nil && s.Items.Ref != nil &&
        s.Items.Ref.Identifier == "#/components/schemas/RichText"
})

The first schema the function accepts becomes the component, so it should accept only schemas that are the same. A description stays where it was, on the reference, since it says what the schema is used for there. Schemas already in components.schemas are left alone.

It fails, changing nothing, with ErrSchemaExists or ErrInvalidSchemaName for the name, as a rename does, and with ErrNoMatch if the function accepts no schema.

Redirecting a schema onto another

RenameSchema refuses to rename a schema onto a name that already exists (ErrSchemaExists). RedirectSchema is for when that's exactly the point — several near-duplicate schemas, typically ones an OpenAPI generator produced one per endpoint that happen to describe the same thing, are being consolidated onto one of them:

// Repoints every reference to "GetPetOkResponse" at "Pet", then removes
// "GetPetOkResponse" from components.schemas.
if err := edit.RedirectSchema(doc, "GetPetOkResponse", "Pet", ""); err != nil {
    log.Fatal(err)
}

What it actually does, precisely — this is a rewrite of references, not a combination of content:

  1. It finds every $ref in the document whose value is "#/components/schemas/GetPetOkResponse" (via the same [walkSchemas] traversal RenameSchema uses) and rewrites each one to "#/components/schemas/Pet", now resolving to Pet. A discriminator's mapping value naming GetPetOkResponse, by name or by reference, is rewritten the same way, and so it is by RenameSchema. A discriminator that selected GetPetOkResponse by its name alone, with no mapping entry, gains one (GetPetOkResponse: Pet), so a payload naming it still selects the schema it meant. Both functions do this.
  2. A oneOf or anyOf that listed both GetPetOkResponse and Pet now lists Pet twice, so it keeps only the first of those plain references. Two alternatives of the same schema are no alternative at all: a value matching one matches the other, so a oneOf could never hold for it. A reference with keywords of its own beside the $ref is kept, and a union the redirect did not change is left alone.
  3. It deletes the "GetPetOkResponse" entry from components.schemas.
  4. It does not look at, merge, or otherwise change the content of either schema. Pet's definition (its properties, its bounds, its wording) is whatever it already was, byte for byte; GetPetOkResponse's definition is simply gone, not folded into Pet's.

If GetPetOkResponse carried bounds or wording worth keeping, pass it as description instead of an empty string: it becomes the description beside the $ref of every reference this repoints, replacing whatever description that reference already had. That's the one piece of GetPetOkResponse this function can carry forward — everything else about its definition is discarded the moment step 3 above runs, so this is the last chance to keep any of it on the sites that used it.

Redirecting a schema onto itself does nothing and reports no error. Otherwise it fails, changing nothing at all, if either name is not in components.schemas (ErrSchemaNotFound).

Many at once

Each call walks the whole document, so a caller consolidating hundreds of schemas should hand them over together. RenameSchemas and RedirectSchemas take a map from old name to new and walk the document a fixed number of times however many names the map holds:

if err := edit.RedirectSchemas(doc, map[string]string{
    "GetPetOkResponse":   "Pet",
    "ListPetsOkItem":     "Pet",
    "GetOwnerOkResponse": "Owner",
}); err != nil {
    log.Fatal(err)
}

The result is the same as calling the single-name function for each entry, and a failure again changes nothing. RenameSchemas renames all its schemas together, so two can swap names; two taking the same new name is ErrSchemaExists. RedirectSchemas refuses to redirect onto a schema it is also redirecting away (ErrSchemaNotFound), since that schema would not be there afterwards.

This is deliberately not the same operation as combining two schemas into one wider shape (adding one's properties, enum values, etc. to the other) — that's openapi-merge's job, and it works on two schema values directly rather than on a document and its references. The two are meant to compose at the call site rather than one wrapping the other: a caller deduplicating a specification decides, using whatever means it likes (openapi-merge included), which of two schemas should survive and what its content should be, then calls RedirectSchema to point every reference at the survivor and drop the one that lost. See Scope below.

Counting and describing references

CountReferences counts the references to each component schema, wherever in the document they occur.

DescribeReferences gives every reference to the named schemas a description beside the $ref, unless the reference has one of its own. A description says what a schema is used for in one place, so before redirecting schemas that describe the same shape onto one, describing the references to each keeps what each meant where it was used:

if err := edit.DescribeReferences(doc, map[string]string{
    "BotWorkspaceName": "The name of the bot's workspace.",
}); err != nil {
    log.Fatal(err)
}

It fails, changing nothing, if a name is not in components.schemas (ErrSchemaNotFound).

Removing what nothing refers to

An edit can leave components that nothing refers to anymore, such as the parts of a schema that has been rewritten. RemoveUnreferenced removes those of the names it is given that nothing refers to, by a $ref or in a discriminator's mapping, and returns them:

removed := edit.RemoveUnreferenced(doc, "PageAllOf0", "PageAllOf1")

It repeats until each name that remains is referred to, since removing one can leave another without a reference. A component not among the names stays, referred to or not, since a specification may define one only to document it.

Scope

Operations belong here when they satisfy two conditions: they mutate a document, and doing them correctly requires knowledge of the document beyond the node being changed.

In scope

  • ✅ Renaming a component and rewriting every reference to it (RenameSchema, RenameSchemas)
  • ✅ Repointing every reference to a duplicate component onto the one that survives, and removing the duplicate (RedirectSchema, RedirectSchemas)
  • ✅ Moving inline definitions into components, replacing each with a reference (ExtractSchema)
  • ✅ Counting the references to each component (CountReferences), and describing them where they are used (DescribeReferences)
  • ✅ Removing the components an edit left without a reference (RemoveUnreferenced)

Out of scope

  • Deciding whether two things should be merged — that is openapi-compare
  • Combining two independently inferred schemas into one wider schema that covers what both described — that is openapi-merge. RedirectSchema above is a different, narrower operation: it never reads or changes either schema's own definition, only the references that pointed at the one being discarded.
  • Whole-document policies such as flattening or deduplication — those are their own modules, and they are expected to use this one
  • Anything universal enough to belong on the types themselves — that goes into openapi instead, so that users who only want to parse and validate a spec aren't made to carry it

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 (this module) 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 Generate Go types, clients, and servers from a specification

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.

Documentation

Overview

Package edit provides structural edits to an OpenAPI document — changes where touching one place obliges you to touch several others.

Index

Constants

View Source
const DefaultMaxArrayExamples = 3

DefaultMaxArrayExamples is the array length TrimExample and TrimSchemaExamples use when the caller passes 0 for maxItems.

Variables

View Source
var ErrNoMatch = errors.New("no inline schema matches")

ErrNoMatch is returned when no inline schema matches, so there is nothing to extract.

Functions

func CountReferences

func CountReferences(doc *openapi.Document) map[string]int

CountReferences counts the references to each schema in components.schemas, wherever in the document they occur. A schema nothing refers to is not in the result.

func DescribeReferences

func DescribeReferences(doc *openapi.Document, descriptions map[string]string) error

DescribeReferences gives every reference to a schema in descriptions the description listed for it, beside the $ref, unless the reference has a description of its own already.

A description says what a schema is used for in one place. Before schemas that describe the same shape are consolidated onto one, describing the references to each keeps what each meant where it was used.

It fails, changing nothing, if a name in descriptions is not in components.schemas (ErrSchemaNotFound).

func ExtractSchema

func ExtractSchema(doc *openapi.Document, name string, match func(*openapi.Schema) bool) error

ExtractSchema moves the inline schemas match accepts into components.schemas, as one schema called name, and replaces each with a reference to it.

The first schema match accepts, in the order the document holds them, becomes the component, so match should accept only schemas that are the same: every one it accepts is replaced, and what set the others apart is lost. Only their descriptions are kept, on the references that replace them, since a description says what a schema is used for there, not what it is.

The schemas already in components.schemas are not inline, so match is never asked about them. It is asked about schemas within them, and anywhere else in the document. A schema it accepts inside another it accepts is part of that one, and moves into the component with it, rather than becoming a reference to the component itself.

It fails, changing nothing, if name is already taken (ErrSchemaExists), could not be referenced (ErrInvalidSchemaName), or if match accepts no schema (ErrNoMatch).

func RedirectSchema

func RedirectSchema(doc *openapi.Document, oldName, newName, description string) error

RedirectSchema repoints every reference to oldName at newName and removes oldName from components.schemas. It does not read or change either schema's own definition — newName's shape is left exactly as it was, and oldName's is discarded along with oldName itself, not merged into newName's. Combining two schemas' definitions into one wider shape is a distinct, unrelated operation; see openapi-merge for that.

It differs from RenameSchema, which refuses to rename a schema onto a name that already exists (ErrSchemaExists): redirecting onto an existing schema is exactly the point here, typically because several near-duplicate schemas (e.g. ones an OpenAPI generator produced one per endpoint, that happen to describe the same thing) are being consolidated onto one of them.

If description is non-empty, it becomes the $ref-level description on every reference this repoints, replacing whatever description that reference already had. The usual reason to set it is that oldName's own definition — its bounds, its wording — is about to be discarded once oldName is gone; setting description is how that information survives on the references that used it, rather than being lost along with oldName.

A oneOf or anyOf that listed both schemas lists newName twice afterwards, so it keeps only the first of those plain references: a value matching one would match the other, and a oneOf could never hold for it.

It fails, changing nothing, if oldName or newName is not in components.schemas (ErrSchemaNotFound).

func RedirectSchemas

func RedirectSchemas(doc *openapi.Document, to map[string]string) error

RedirectSchemas is RedirectSchema for many schemas at once: it repoints every reference to each key of to at that key's value, and removes the keys from components.schemas.

It walks the document a fixed number of times however many schemas it redirects, where calling RedirectSchema for each would walk it once per schema.

A key redirected onto itself is left alone. Otherwise it fails, changing nothing, if a key or a value is not in components.schemas, or if a value is itself redirected and so would not be there afterwards (ErrSchemaNotFound).

func RemoveUnreferenced

func RemoveUnreferenced(doc *openapi.Document, names ...string) []string

RemoveUnreferenced removes those of the schemas names from components.schemas that nothing in doc refers to, and returns the ones it removed. A schema a discriminator's mapping names is referred to, as much as by a $ref.

It repeats until each of names that remains is referred to, since removing one can leave another without a reference. A schema not among names stays, referred to or not: a specification may define one only to document it.

func RenameSchema

func RenameSchema(doc *openapi.Document, oldName, newName string) error

RenameSchema renames a schema in components.schemas and rewrites every reference to it, wherever in the document that reference occurs.

The schema keeps its position among the components, so renaming produces a one-line change rather than reordering the section.

Renaming a schema to its current name does nothing and reports no error. Otherwise it fails, changing nothing, if:

  • oldName is not in components.schemas (ErrSchemaNotFound);
  • newName is already taken (ErrSchemaExists) — renaming onto an existing schema would silently discard one of two different definitions, and point every reference to whichever survived;
  • newName could not be referenced (ErrInvalidSchemaName).

func RenameSchemas

func RenameSchemas(doc *openapi.Document, to map[string]string) error

RenameSchemas is RenameSchema for many schemas at once: it renames each key of to that key's value, and rewrites every reference to it.

The renames happen together, so a new name may be one another key is giving up, and two schemas can swap names. It walks the document a fixed number of times however many schemas it renames, where calling RenameSchema for each would walk it once per schema.

It fails, changing nothing, for the reasons RenameSchema does, and with ErrSchemaExists when two keys would take the same new name.

func TrimExample

func TrimExample(v jsontext.Value, maxItems int) (jsontext.Value, error)

TrimExample returns v with every array it contains, at any depth, cut down to at most maxItems elements. Object key order and every kept value's own bytes are preserved exactly; only arrays are shortened. If maxItems is 0, DefaultMaxArrayExamples is used.

Elements are chosen to be representative rather than just the first few: an array keeps one element per distinct shape it observes first — an object's own set of keys, or a scalar's JSON type — in the order they appear, before repeating one to fill the rest of the budget. A field only some elements carry, or a value that is sometimes a number and sometimes a string, therefore survives trimming instead of being cut away by chance.

v must be well-formed JSON; TrimExample returns an error otherwise.

func TrimSchemaExamples

func TrimSchemaExamples(doc *openapi.Document, maxItems int) error

TrimSchemaExamples applies TrimExample to every schema's own Example reachable from doc, in place. If maxItems is 0, DefaultMaxArrayExamples is used.

Types

type ErrInvalidSchemaName

type ErrInvalidSchemaName struct{ Name string }

ErrInvalidSchemaName is returned when the new name is not a valid key under components, and so could not be referenced.

func (*ErrInvalidSchemaName) Error

func (e *ErrInvalidSchemaName) Error() string

type ErrSchemaExists

type ErrSchemaExists struct{ Name string }

ErrSchemaExists is returned when the new name is already taken by another schema. Renaming onto it would silently merge two definitions into one.

func (*ErrSchemaExists) Error

func (e *ErrSchemaExists) Error() string

type ErrSchemaNotFound

type ErrSchemaNotFound struct{ Name string }

ErrSchemaNotFound is returned when the schema to rename is not in components.schemas.

func (*ErrSchemaNotFound) Error

func (e *ErrSchemaNotFound) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL