reference

package
v1.35.0 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package reference renders an OpenAPI 3.x document as a self-hosted API reference page: an alternative to Scalar and Redoc that ships inside the backend, with no CDN assets.

The model below covers the subset of OpenAPI 3.0 and 3.1 that matters for documentation. Unknown fields are ignored. Values that differ between 3.0 and 3.1 (nullable versus "null" in the type list, exclusiveMinimum as a bool versus a number) are normalized by Parse so the renderer sees one shape.

Index

Constants

View Source
const (
	EdgeHasOne  = "has-one"
	EdgeHasMany = "has-many"
	EdgeFK      = "fk"
)

Edge kinds.

Variables

This section is empty.

Functions

func Assets

func Assets() http.Handler

Assets serves reference.css and reference.js from the embedded files, matched on the last path segment so it works with or without http.StripPrefix; the ?v= version query that Render appends is ignored. Every other path is a 404. Responses carry an ETag and a short max-age, so a deploy is picked up within minutes and revalidation is a cheap 304.

Mount Assets on a public route: the page loads it with <link> and <script src>, which cannot send an Authorization header.

func AssetsHandler

func AssetsHandler() echo.HandlerFunc

AssetsHandler adapts Assets to Echo, for routes like e.GET("/docs/assets/*", reference.AssetsHandler()) paired with Options.AssetsURL = "/docs/assets".

func ParamString

func ParamString(v any) string

ParamString renders an example value as text for a URL, header or form field: nil as "", strings verbatim, booleans as true or false, numbers without an exponent (1000000, not 1e+06), arrays comma-joined, and objects (*Ordered[any], map[string]any, map[any]any) as compact JSON.

func ParamValues

func ParamValues(p *Parameter, v any) []string

ParamValues serializes an example value of p into the values sent under p.Name, following the OpenAPI style and explode rules for arrays so the curl sample and the try-it client agree:

  • style defaults to form for query and cookie parameters and to simple for path and header parameters;
  • explode defaults to true for form and false for every other style;
  • an exploded array of a query style (form, pipeDelimited, spaceDelimited) yields one value per item, which the caller sends as repeated keys (a=1&a=2);
  • any other array yields a single value joined with "," (form, simple and anything unrecognized), "|" (pipeDelimited) or " " (spaceDelimited).

Items and non-array values are rendered with ParamString, so objects travel as compact JSON; the deepObject, label and matrix styles are not modeled. An exploded empty array yields no values.

func RefName

func RefName(ref, prefix string) string

RefName returns the component name a local ref points at, or "" when ref is empty or not of the given kind.

func Render

func Render(doc *Document, opts Options) ([]byte, error)

Render renders doc as a complete HTML page.

func ToJSON

func ToJSON(v any) string

ToJSON renders an example value as JSON indented with two spaces, keeping *Ordered[any] key order and leaving <, > and & unescaped. nil renders as null.

Types

type AdditionalProperties

type AdditionalProperties struct {
	Allowed bool
	Schema  *Schema
}

AdditionalProperties decodes either a boolean or a schema.

func (*AdditionalProperties) UnmarshalJSON

func (a *AdditionalProperties) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts true, false or a schema object.

func (*AdditionalProperties) UnmarshalYAML

func (a *AdditionalProperties) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts true, false or a schema mapping.

type Bound

type Bound struct {
	Flag  bool
	Value *float64
}

Bound decodes exclusiveMinimum and exclusiveMaximum, which are booleans in 3.0 (qualifying minimum and maximum) and numbers in 3.1.

func (*Bound) UnmarshalJSON

func (b *Bound) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts a bool or a number.

func (*Bound) UnmarshalYAML

func (b *Bound) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts a bool or a number.

type Change

type Change struct {
	Kind ChangeKind
	// Path locates the change, segments joined by " › ", for example
	// "GET /issues/{id} › response 200 › created_at" or "Issue › status".
	Path string
	// Detail says what changed: "property removed", "type changed from
	// string to integer", "model Issue changed".
	Detail string
	// Breaking is set for changes that can break existing clients.
	Breaking bool
	// contains filtered or unexported fields
}

Change is one difference between the base and the head document.

type ChangeKind

type ChangeKind int

ChangeKind says whether something was added, removed or changed between two versions of a document.

const (
	Added ChangeKind = iota + 1
	Removed
	Changed
)

func (ChangeKind) String

func (k ChangeKind) String() string

String returns "added", "removed" or "changed".

type Components

type Components struct {
	Schemas         Ordered[*Schema]         `json:"schemas" yaml:"schemas"`
	Responses       Ordered[*Response]       `json:"responses" yaml:"responses"`
	Parameters      Ordered[*Parameter]      `json:"parameters" yaml:"parameters"`
	Examples        Ordered[*Example]        `json:"examples" yaml:"examples"`
	RequestBodies   Ordered[*RequestBody]    `json:"requestBodies" yaml:"requestBodies"`
	Headers         Ordered[*Header]         `json:"headers" yaml:"headers"`
	SecuritySchemes Ordered[*SecurityScheme] `json:"securitySchemes" yaml:"securitySchemes"`
}

Components holds the reusable objects $ref points at.

type Contact

type Contact struct {
	Name  string `json:"name" yaml:"name"`
	URL   string `json:"url" yaml:"url"`
	Email string `json:"email" yaml:"email"`
}

Contact is info.contact.

type Discriminator

type Discriminator struct {
	PropertyName string            `json:"propertyName" yaml:"propertyName"`
	Mapping      map[string]string `json:"mapping" yaml:"mapping"`
}

Discriminator is schema.discriminator.

type Document

type Document struct {
	OpenAPI    string                `json:"openapi" yaml:"openapi"`
	Info       Info                  `json:"info" yaml:"info"`
	Servers    []Server              `json:"servers" yaml:"servers"`
	Paths      Ordered[*PathItem]    `json:"paths" yaml:"paths"`
	Components Components            `json:"components" yaml:"components"`
	Security   []map[string][]string `json:"security" yaml:"security"`
	Tags       []Tag                 `json:"tags" yaml:"tags"`
}

Document is a parsed OpenAPI document.

func Parse

func Parse(data []byte) (*Document, error)

Parse decodes an OpenAPI 3.x document from YAML or JSON and normalizes the 3.0/3.1 differences described on Schema. Every failure is an ez.EINVALID error whose message says what is wrong with the input.

func (*Document) Curl

func (d *Document) Curl(method, path string, item *PathItem, op *Operation, server string) string

Curl builds a multi-line curl command for the operation.

The base URL is chosen in this order: the server argument, an explicit override such as Options.ServerURL; the operation's first server; the document's first server; otherwise none, which leaves a relative URL. Pass "" for server when there is no override so operation servers win over document servers. Declared servers have their {variables} replaced by their defaults, and so does a server argument equal to a declared server URL.

The base URL is joined with path, with path parameters replaced by their examples and required query parameters appended, serialized as ParamValues describes. Credentials from the first security requirement appear as shell variables such as $API_KEY or $TOKEN, double-quoted so the shell expands them. The body is the example of the first request content type: JSON as -d, multipart/form-data as one --form-string per property (-F field=@path for binary properties), form-urlencoded as one --data-urlencode per property, and anything else as --data-binary '@file'.

func (*Document) Example

func (d *Document) Example(e *Example) *Example

Example resolves an example ref.

func (*Document) ExampleJSON

func (d *Document) ExampleJSON(s *Schema, mode ExampleMode) string

ExampleJSON renders ExampleValue as indented JSON.

func (*Document) ExampleValue

func (d *Document) ExampleValue(s *Schema, mode ExampleMode) any

ExampleValue builds a sample value for s the way Redoc and Scalar do.

Explicit values win in the order example, examples[0], default, const, enum[0]; otherwise a value is generated from the type and format. Objects are returned as *Ordered[any] so properties keep their declared order when passed to ToJSON. oneOf and anyOf use their first member. A named schema that recurs on the current path yields an empty value of its type ({} for objects, [] for arrays, nil for scalars). Generated values nest at most three named object schemas deep.

func (*Document) Flatten

func (d *Document) Flatten(s *Schema) *Schema

Flatten resolves s and merges its allOf members into a single schema, the way Redoc presents composed objects. The document is never mutated: a schema without allOf is returned as resolved, unless it holds its lower or upper bound in two keywords, see below, and otherwise the result is a fresh shallow copy. Treat the result as read-only either way. Members are resolved and flattened recursively; a member seen earlier in the same chain is not expanded again, so cyclic allOf definitions terminate. A $ref is followed and the keywords beside it are dropped.

Merge rules: properties are unioned in order, parent first then members in order, and a later definition of the same property replaces the earlier one while keeping its position. Required is unioned. Every member applies, so of the numeric bounds the tightest is kept: of minLength, maxLength, minItems, maxItems, minProperties and maxProperties each, and of the lower bound minimum and exclusiveMinimum (and the upper bound maximum and exclusiveMaximum) together, kept in the one keyword that holds it, the exclusive one at equal values, in its numeric form. Other scalar keywords take the first non-empty value walking parent first then members in order; boolean flags (nullable, deprecated, readOnly, writeOnly, uniqueItems) are true when any of them sets it. oneOf, anyOf, discriminator and additionalProperties of members are kept only when the parent has none of its own.

A member's oneOf, anyOf and discriminator are dropped when they point back at the schema being flattened or at any schema in the current allOf chain. That is the springdoc and NSwag polymorphism shape, Pet: {oneOf: [Cat, Dog], discriminator} with Cat: {allOf: [Pet, {...}]}: Cat is one concrete variant, not another "one of Cat or Dog".

func (*Document) Graph

func (d *Document) Graph() *Graph

Graph builds the entity graph of the component schemas.

When any component schema declares the table it is stored in with an x-table extension (see Document.Table), the entities are exactly those schemas. Otherwise they are guessed: an entity is a component schema that flattens to an object (not an enum) and either has an id property or is referenced by a property of another entity. Names ending in Request, Response, Params, Query or Body are never guessed entities, and a wrapper that only operations use has no id and no entity pointing at it, so it is left out as well.

Edges come, in order of confidence, from an x-ref extension on a property (a $ref string naming the target), from a property that is a $ref to an entity (has-one) or an array of them (has-many), from a foo_id property next to such a foo property (the foreign key of that relation), and last from a foo_id property alone, matched by name to an entity called Foo (marked Inferred). A has-many property is left out when its target holds a foreign key back, as the other side of that key's relation.

func (*Document) Header

func (d *Document) Header(h *Header) *Header

Header resolves a header ref.

func (*Document) MediaExample

func (d *Document) MediaExample(mt *MediaType, mode ExampleMode) (any, bool)

MediaExample picks the example for a media type: an explicit example, the first named example, or a value generated from the schema. ok is false when the media type has none of these.

func (*Document) MediaExampleJSON

func (d *Document) MediaExampleJSON(mt *MediaType, mode ExampleMode) string

MediaExampleJSON renders MediaExample as indented JSON, or "" when the media type has no example.

func (*Document) OperationSecurity

func (d *Document) OperationSecurity(op *Operation) []map[string][]string

OperationSecurity returns the security requirements that apply to op: its own when set, otherwise the document's.

func (*Document) Operations

func (d *Document) Operations() []OperationRef

Operations lists every operation in document path order, then in the canonical method order of PathItem.Methods.

func (*Document) Parameter

func (d *Document) Parameter(p *Parameter) *Parameter

Parameter resolves a parameter ref.

func (*Document) Parameters

func (d *Document) Parameters(item *PathItem, op *Operation) []*Parameter

Parameters returns the parameters that apply to op: the path item's merged with the operation's, where an operation parameter replaces a path parameter of the same name and location in place. Refs are resolved. The result is grouped by location (path, query, header, cookie, then anything else) and keeps document order within each group. Either argument may be nil.

func (*Document) RequestBody

func (d *Document) RequestBody(b *RequestBody) *RequestBody

RequestBody resolves a request body ref.

func (*Document) Response

func (d *Document) Response(r *Response) *Response

Response resolves a response ref.

func (*Document) Schema

func (d *Document) Schema(s *Schema) (*Schema, string)

Schema follows s through any chain of $ref and returns the target together with the name of the last component visited ("" for inline schemas). A ref that cannot be resolved returns s itself with the name it pointed at, so the renderer can still show something.

func (*Document) SchemaName

func (d *Document) SchemaName(s *Schema) string

SchemaName returns the component name s stands for: the name from Schema, or, when s is an inline wrapper whose only composition is a single-member allOf (the 3.0 idiom for adding description or nullable to a $ref), the name of that member. Cycle guards must use this name rather than the one from Schema, otherwise a model that refers to itself through such a wrapper is never recognised as recursive.

func (*Document) Table

func (d *Document) Table(name string) string

Table returns the database table the component schema called name is stored in, as declared by its x-table extension, or "" when it declares none. Schemas without one are shapes that only travel over HTTP.

type Edge

type Edge struct {
	From        string `json:"from"`
	To          string `json:"to"`
	FromField   string `json:"fromField,omitempty"`
	Kind        string `json:"kind"`
	Cardinality string `json:"cardinality"`
	Inferred    bool   `json:"inferred,omitempty"`
	Diff        string `json:"diff,omitempty"`
}

Edge is a relation from the entity holding the field to the entity it points at. Inferred is true when the relation was guessed from a foo_id field name rather than declared by a $ref or an x-ref extension. Diff is set in diff mode only, as on Node.

type Example

type Example struct {
	Ref           string `json:"$ref" yaml:"$ref"`
	Summary       string `json:"summary" yaml:"summary"`
	Description   string `json:"description" yaml:"description"`
	Value         any    `json:"value" yaml:"value"`
	ExternalValue string `json:"externalValue" yaml:"externalValue"`
}

Example is a named example.

type ExampleMode

type ExampleMode int

ExampleMode selects which side of the wire an example is for: request examples drop readOnly properties, response examples drop writeOnly ones.

const (
	ForRequest ExampleMode = iota
	ForResponse
)

type Field

type Field struct {
	Name       string   `json:"name"`
	Type       string   `json:"type,omitempty"`
	Optional   bool     `json:"optional,omitempty"`
	Nullable   bool     `json:"nullable,omitempty"`
	ReadOnly   bool     `json:"readOnly,omitempty"`
	WriteOnly  bool     `json:"writeOnly,omitempty"`
	Deprecated bool     `json:"deprecated,omitempty"`
	Enum       []string `json:"enum,omitempty"`
	Ref        string   `json:"ref,omitempty"`
	Diff       string   `json:"diff,omitempty"`
}

Field is one property of an entity. Optional means it is not listed in required. Ref names the entity the field points at, either as an embedded object or array or as a foreign key. Diff is set in diff mode only, as on Node.

type Graph

type Graph struct {
	Nodes []*Node `json:"nodes"`
	Edges []*Edge `json:"edges"`
}

Graph is the entity-relationship view of a document's component schemas: one node per entity and one edge per relation between two entities.

type Header = Parameter

Header is a response header; the same shape as Parameter without name and in.

type Info

type Info struct {
	Title          string   `json:"title" yaml:"title"`
	Version        string   `json:"version" yaml:"version"`
	Summary        string   `json:"summary" yaml:"summary"`
	Description    string   `json:"description" yaml:"description"`
	TermsOfService string   `json:"termsOfService" yaml:"termsOfService"`
	Contact        *Contact `json:"contact" yaml:"contact"`
	License        *License `json:"license" yaml:"license"`
}

Info is the document header.

type License

type License struct {
	Name string `json:"name" yaml:"name"`
	URL  string `json:"url" yaml:"url"`
}

License is info.license.

type MediaType

type MediaType struct {
	Schema   *Schema           `json:"schema" yaml:"schema"`
	Example  any               `json:"example" yaml:"example"`
	Examples Ordered[*Example] `json:"examples" yaml:"examples"`
}

MediaType is one content type of a body or response.

type MethodOperation

type MethodOperation struct {
	Method    string
	Operation *Operation
}

MethodOperation pairs an HTTP method with its operation.

type ModelDiff

type ModelDiff struct {
	Name     string
	Kind     ChangeKind
	Breaking bool
	Changes  []Change
}

ModelDiff is the result for one component schema that was added, removed or changed; its Changes end with a "model X changed" entry per changed model it references. A model also counts as changed when a model it references changed, so a change inside StandardError marks ErrorResponse and every operation that returns it.

type Node

type Node struct {
	Name   string  `json:"name"`
	Short  string  `json:"short"`
	Group  string  `json:"group,omitempty"`
	Anchor string  `json:"anchor"`
	Table  string  `json:"table,omitempty"`
	Hub    bool    `json:"hub,omitempty"`
	Diff   string  `json:"diff,omitempty"`
	Fields []Field `json:"fields"`
}

Node is one entity. Name is the component name, Short its display name, Group the module it belongs to ("" when the names carry none) and Anchor the id of its model card in the reference page. Table is the database table the schema declares with x-table ("" when it declares none). Hub marks an entity that most others point at (see hubSources); the map leaves its edges out until asked. Diff is set in diff mode only (see Options.Base): diffAdded, diffChanged or diffRemoved.

type Operation

type Operation struct {
	Tags        []string           `json:"tags" yaml:"tags"`
	Summary     string             `json:"summary" yaml:"summary"`
	Description string             `json:"description" yaml:"description"`
	OperationID string             `json:"operationId" yaml:"operationId"`
	Parameters  []*Parameter       `json:"parameters" yaml:"parameters"`
	RequestBody *RequestBody       `json:"requestBody" yaml:"requestBody"`
	Responses   Ordered[*Response] `json:"responses" yaml:"responses"`
	Deprecated  bool               `json:"deprecated" yaml:"deprecated"`
	// Internal is the x-internal extension: the operation is documented for
	// the team, not for API consumers.
	Internal bool `json:"x-internal" yaml:"x-internal"`
	// Destructive is the x-destructive extension: a non-DELETE operation
	// whose effect destroys data irreversibly, such as configuring a policy
	// that triggers deletion. It is meant for write operations: a GET or
	// HEAD operation stays read-only whether or not it carries the mark.
	Destructive bool     `json:"x-destructive" yaml:"x-destructive"`
	Servers     []Server `json:"servers" yaml:"servers"`
	// Security is nil when the operation inherits the document security, and
	// an empty slice when the operation is explicitly public.
	Security *[]map[string][]string `json:"security" yaml:"security"`
}

Operation is one HTTP method of a path.

type OperationDiff

type OperationDiff struct {
	Method   string
	Path     string
	Summary  string
	Tags     []string
	Kind     ChangeKind
	Breaking bool
	// Changes lists the operation's own changes, then one "model X
	// changed" entry per changed model it uses.
	Changes []Change
}

OperationDiff is the result for one operation that was added, removed or changed. Operations are keyed by method and path.

func (*OperationDiff) Key

func (o *OperationDiff) Key() string

Key returns "METHOD /path".

type OperationRef

type OperationRef struct {
	Method    string
	Path      string
	Item      *PathItem
	Operation *Operation
}

OperationRef is one operation of the document with the path it lives on.

type Options

type Options struct {
	// Title is the <title> of the page; defaults to info.title.
	Title string
	// AssetsURL is the base URL serving reference.css and reference.js (see
	// Assets). Empty inlines both into the page, which then needs no other
	// request but also needs a CSP that allows inline scripts. Use an
	// absolute URL when the page is embedded through iframe srcdoc: a
	// relative one resolves against the host page. The asset route must be
	// public, because <script src> and <link> cannot send auth headers.
	AssetsURL string
	// SpecURL is an optional link to the raw spec shown in the header; empty
	// hides it.
	SpecURL string
	// ServerURL is the base URL for curl samples and the default server of
	// the try-it client. Empty uses the operation's servers[0], then the
	// document's servers[0] (variables replaced by their defaults), then the
	// page origin. Because try-it otherwise defaults to servers[0], usually
	// production, a staging deployment must set it.
	ServerURL string
	// Base is an optional previous version of the spec (YAML or JSON). When
	// set, the page shows what changed since it: badges on operations and
	// models, marked rows in schema trees and a "What changed" section. See
	// Diff for the rules.
	Base []byte
}

Options configures Render and New.

Embedding

To show the page inside another app (emma-webapp does), fetch the HTML with the user's credentials and put it in <iframe srcdoc="..." sandbox="allow-scripts allow-popups">. Leave out allow-same-origin so the docs cannot read the host's cookies or storage; try-it credentials then live in the iframe's own in-memory store (the script falls back to memory when storage is unavailable) and are lost on reload. Set AssetsURL to an absolute URL on a public route, and set ServerURL, since a srcdoc page has no origin of its own.

type Ordered

type Ordered[T any] struct {
	Keys []string
	Map  map[string]T
}

Ordered is a map that remembers the order keys appeared in the document. OpenAPI tooling such as Redoc renders properties, paths and responses in authoring order, and so do we.

func (*Ordered[T]) Get

func (o *Ordered[T]) Get(key string) (T, bool)

Get returns the value for key.

func (*Ordered[T]) Len

func (o *Ordered[T]) Len() int

Len returns the number of entries.

func (Ordered[T]) MarshalJSON

func (o Ordered[T]) MarshalJSON() ([]byte, error)

MarshalJSON writes entries in order.

func (*Ordered[T]) Set

func (o *Ordered[T]) Set(key string, value T)

Set appends key when new and stores value.

func (*Ordered[T]) UnmarshalJSON

func (o *Ordered[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps object key order. A JSON null decodes as empty.

func (*Ordered[T]) UnmarshalYAML

func (o *Ordered[T]) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML keeps mapping order. Merge keys (<<: *anchor) are expanded in place; keys written in the mapping itself win over merged ones, and earlier merged mappings win over later ones, as the YAML merge spec says.

type Page

type Page struct {
	HTML []byte
	ETag string
}

Page is a reference page rendered once, typically at startup, and served from memory.

func New

func New(spec []byte, opts Options) (*Page, error)

New parses spec (YAML or JSON) and renders it with opts.

func (*Page) Handler

func (p *Page) Handler() echo.HandlerFunc

Handler serves the page as text/html. Clients that send the current ETag in If-None-Match get 304 Not Modified. The page is marked private because the spec behind it is usually served to authenticated users only.

type Parameter

type Parameter struct {
	Ref         string              `json:"$ref" yaml:"$ref"`
	Name        string              `json:"name" yaml:"name"`
	In          string              `json:"in" yaml:"in"`
	Description string              `json:"description" yaml:"description"`
	Required    bool                `json:"required" yaml:"required"`
	Deprecated  bool                `json:"deprecated" yaml:"deprecated"`
	Style       string              `json:"style" yaml:"style"`
	Explode     *bool               `json:"explode" yaml:"explode"`
	Schema      *Schema             `json:"schema" yaml:"schema"`
	Example     any                 `json:"example" yaml:"example"`
	Examples    Ordered[*Example]   `json:"examples" yaml:"examples"`
	Content     Ordered[*MediaType] `json:"content" yaml:"content"`
}

Parameter is a path, query, header or cookie parameter.

type PathItem

type PathItem struct {
	Ref         string       `json:"$ref" yaml:"$ref"`
	Summary     string       `json:"summary" yaml:"summary"`
	Description string       `json:"description" yaml:"description"`
	Get         *Operation   `json:"get" yaml:"get"`
	Put         *Operation   `json:"put" yaml:"put"`
	Post        *Operation   `json:"post" yaml:"post"`
	Delete      *Operation   `json:"delete" yaml:"delete"`
	Options     *Operation   `json:"options" yaml:"options"`
	Head        *Operation   `json:"head" yaml:"head"`
	Patch       *Operation   `json:"patch" yaml:"patch"`
	Trace       *Operation   `json:"trace" yaml:"trace"`
	Parameters  []*Parameter `json:"parameters" yaml:"parameters"`
}

PathItem is one entry of paths.

func (*PathItem) Methods

func (p *PathItem) Methods() []MethodOperation

Methods lists the operations of the path item in the canonical HTTP order.

type Report

type Report struct {
	// Operations holds changed, added and removed operations in head
	// document order; removed operations follow the operation that preceded
	// them in the base document.
	Operations []*OperationDiff
	// Models holds changed, added and removed component schemas in head
	// order, then the removed ones in base order.
	Models []*ModelDiff
	// Changes is every change, operations first, then models.
	Changes []Change
	// contains filtered or unexported fields
}

Report is the structural difference between a base (older) and a head (newer) OpenAPI document.

func Diff

func Diff(base, head *Document) *Report

Diff compares two documents structurally: operations by method and path, schemas property by property after allOf flattening and ref resolution. Flattening keeps the tightest of the bounds allOf members set, and the keywords written beside a $ref apply together with the model it names, on a schema and on each allOf member alike.

Where both sides refer to the same model, under any spelling of the reference, as a $ref or as single-member allOf wrappers nested around one, with only keywords beside the reference and on each wrapper that give it no structure of its own, that is none of properties, required, items, enum, oneOf, anyOf, not, discriminator and additionalProperties, the model is not expanded: what applies there, the keywords with the model, is compared, the model itself once on its own, and every operation or model that reaches a changed model through references is reported as changed. The use reports a change only when its own keywords change what applies there, compared with the model as the head document has it, so a change to the model alone is reported in the model only; it then reports what applied there before, its keywords with the model as the base document has it, against what applies after. Any other shape is compared in full, such as an allOf of a reference and other members, a reference or a wrapper with structure beside it, or a base reference that leads in the head document to another model than the head side does, and a change inside the model then shows both there and in the model. Two sides whose references in those shapes name the same component, though, refer to that component as the model, also when on one side it only refers to another model, as a component with nothing but a reference does until it gets a keyword of its own, which is then reported once, in that component. A type on one side only, where both sides have oneOf or anyOf variants, as beside a reference to a model that is a oneOf and states no type, is no change when it restates the variants, whether the model is expanded or not: every variant states a type, each of the variant's non-null types is one the type names, an integer counting as a number, and no variant lets null through that the type refuses. Otherwise, as beside variants of string and integer, beside nullable variants, or beside a reference to a model that states no type and has no variants, it is compared like any type.

An allOf of two or more members is nullable when the schema itself says so, or when a member does and every member lets null through, as one that is nullable or that states none of type, enum, const, oneOf, anyOf and not does; a single member, as in the 3.0 spelling of a nullable reference, passes its nullable on.

These limits follow. enum, pattern, multipleOf, const and not, and the oneOf and anyOf lists, like default and the other keywords that cannot be combined across allOf members, are compared as the first one found, the schema's own and then its members' in order, so the list of a second member is not compared. Each keyword is compared on its own, so a change another keyword makes moot is still reported with its own flag: a maxLength changed under an enum whose values all meet both, bounds that admit no value changed for others that admit none either, a const added under an enum of that one value, a minProperties of 1 added where a required property was dropped. A schema with nullable and an enum or const without null is read as letting null through, so nullable removed there is breaking in a request though neither side accepts null, and a oneOf in which two variants let null through is read as letting null through, though the oneOf then refuses it. A schema with a not is read as refusing null, so nullable removed beside a not breaks a request, and given beside one breaks a response, whatever the not refuses. A type of only null is read as the type null, made nullable, so {type: string} or {} to {type: null} reads type changed to null, which narrows, beside made nullable, and the reverse widens, as does {type: null} to {type: [string, null]}, and a oneOf or anyOf of only {type: null} reads as {type: null} does. Given by an allOf member, as in {allOf: [{type: null}]}, or {allOf: [{$ref: N}]} with N {type: null}, it is read as nullable with no type, so {type: string} to that reads type changed from string to any, which widens, though only null is then accepted, and {type: null} to that reads type changed from null to any, breaking a response, though the two say the same. Beside a $ref it is not read as a type either, so {$ref: S} to {$ref: S, type: null}, with S {type: string}, reads made nullable only, which breaks a response only, though nothing is accepted then. Each keyword is compared on its own, so {enum: [null]} or {const: null} to {type: null} reads type changed from any to null, breaking a request, though each accepts only null, and variants given beside type: null narrow, breaking a request only. A {type: null} variant is folded into nullable and is not seen as a type beside the type of its parent, and an enum of only null is read as an enum, not as a type of null. A not is compared as written, see below, so any change inside it is reported once, as not changed, and a model either side names under an odd number of nots, those inside variants counted, that changed is reported where the outermost not stands, or, for a not in a variant of a list whose variants changed otherwise, where that list stands, as not changed: model M changed, its changes judged strictly and with their flags swapped; under an even number it reads model M changed, its changes still judged strictly, with their flags as they are. Judged strictly, enum values added and additional properties allowed again widen the model, and a format added narrows it, one dropped widens it and one changed does both. An optional property added is not judged strictly, though it narrows a model that allows other properties, so under a not in a response it breaks nothing: M: {type: object, properties: {a}} gaining b: {type: integer} refuses {"b": "s"}, which {not: {$ref: M}} then accepts. A type is read as object from properties and additionalProperties and as array from items where none is stated, so a stated type removed beside them is not reported, though the schema then accepts any value; only whether a nullable change is breaking reads no type there. patternProperties is not read, so a property removed from an object that allows no other properties breaks a request even when a pattern still accepts it. Variants added to a oneOf are read as widening, as for an anyOf, though a value that newly matches two of them is refused, a variant listed more or fewer times in a oneOf, which changes the values that match exactly one, breaks nothing, and a list of the same length is compared by position, so variants that were only reordered are reported as changed where they now stand. A comparison that walks references in place, as one of models renamed between the documents, or one through an allOf that joins a model to other members or a reference with structure beside it, which are compared in place even where the documents are the same, can take long: a chain of renamed models in which each refers twice to the next is compared once for every path through it, twice as many with each model; a model whose properties each join it to a bound through an allOf is compared once for every order of those properties, as the walk's cycle guard only stops a schema met again on the same path, so ten such properties take about ten seconds and eleven two minutes, and where each joins it to a not of itself instead, as allOf: [{$ref: M}, {not: {$ref: M}}], the models the nots name are recorded as used at every such path, so memory grows as fast as time: nine take 2 seconds and 0.4 GB, ten 20 seconds and 4.5 GB; a chain of models in which each refers to the next from a property, with structure beside the reference, such as properties, is compared at each model for the rest of the chain, n²/2 schemas for n models, about a second for 2,000, and where what is beside the references changed, the change is reported at every level of every model above it, 2,001,001 changes for 2,000 models, which takes 0.3 seconds and 0.5 GB for 500 models, 2.3 seconds and 3.4 GB for 1,000, and 32 seconds and 24 GB for 2,000; and a chain of models each of which is itself a reference to the next with structure beside it, M0: {$ref: M1, properties: {...}}, takes time cubic in its length, as each model is flattened to the end of the chain, whether or not the documents are the same: seconds for 500 models, tens of seconds for 1,000 and minutes for 2,000, where the same chain written as allOf: [{$ref: M1}, {properties: {...}}] took about as long before too, 2 to 3 seconds for 500 models. A model M changed (through ...) entry names the first 50 of the models it changed through and counts the others. A model that no operation reaches, or that only something the head document added refers to, such as a new operation, property or variant, has no recorded use in a request or a response, so in the Models section, and in the count of breaking changes, each of its changes is breaking when it would break either. A model is used where an operation reaches it also where it is compared in place, its changes then listed there too, as for a property that switched to it from a component that is gone, or one that joins it to other members by an allOf or has structure beside its reference, and so is a component that only refers to another model, as X: {$ref: M} or {allOf: [{$ref: M}]} does, where a reference to it is used, also where one switched to it, or from it, which uses it in the context of the switch, though the use is reported under the model it names. An operation that uses a model reports model M changed with the flag the model's change has in its context, also when keywords at the use mask that change there, as a maxLength of 3 beside a reference does a model's maxLength lowered from 10 to 5.

Breaking changes follow oasdiff. A schema change that narrows the values accepted breaks a request, where a client sending what used to be valid is now refused, and not a response; one that widens them breaks a response, where a client may receive what it was never told to expect, and not a request. Narrowing: a raised minimum or lowered maximum (an exclusive bound is tighter than an inclusive one at the same value, and on a schema of type integer each bound is first taken as the inclusive bound that admits the same integers, so minimum: 1 and exclusiveMinimum: 0 are the same); a raised minLength, minItems or minProperties; a lowered maxLength, maxItems or maxProperties; any of these bounds added, except a minLength, minItems or minProperties of 0, which every value meets; a pattern, multipleOf or const added; uniqueItems set; a type stated where there was none; an enum where there was none, or enum values removed; nullable removed from a schema that then refuses null; a not added; additionalProperties: false, or the false schema written as {not: {}}, with nothing but annotations beside the not or under it, on an object that allowed other properties; items: false, or {not: {}}, on an array that allowed items. Widening: the reverse bound changes, any bound removed, again except a minLength, minItems or minProperties of 0; a pattern, multipleOf or const removed; uniqueItems unset; a type dropped; an enum dropped; nullable added to a schema that refused null; a not dropped; items allowed again. Between two nots, the schemas under them are compared as written, a $ref the same as another where it names the same component, whose model is not compared there, properties, required names, types and enum values in any order, and extensions left out: written the same, they report nothing; different only in annotations, title, description, default, example, examples, deprecated, readOnly and writeOnly, not changed breaks neither; different otherwise, not changed breaks both, as which way a change under a not moves is not worked out, also where only a reference was renamed to a model written the same. Otherwise, a change from one stated type to another, but for a type of only null, see above, and a pattern, multipleOf or const replaced by a different one, break both; enum values added to an enum, additional properties allowed again, as a new response property is, a format change and a default changed, added or removed break neither, and so does a nullable change on a schema that lets null through on both sides, such as one that states no type. When the type changes between integer and number, a bound's detail gives the bound the integer side admitted beside the one written, as in minimum lowered from 0.5 (1 for integers) to 0.7. The schemas of the additional properties of two objects that allow them are compared like any other schemas, under <key>, with the empty schema, which allows any value, standing for additionalProperties: true, {} or none at all, so no change between those three is reported; the items of two schemas of the same type are compared the same way, with the empty schema standing for items absent.

oneOf and anyOf variants are compared by position where both sides have lists of the same keyword and length, each keyword on its own. Any other change of variants is judged from the variants as written, without comparing them, a $ref the same as another when it names the same component, properties, required names, types and enum values in any order, and annotations left out, and breaks both, as which way it moved cannot be told, except in these cases. Variants only added to a list widen, and only removed narrow, and the same variants with one listed more or fewer times break neither. Variants given where there were none, with nothing beside them but annotations and nullable, in place of a schema written as one of them, widen, and dropped back to such a schema narrow; a change of nullable is reported on its own. Variants given beside other keywords narrow, as a value must then match one of them too, and dropped from beside them widen. anyOf turned into oneOf with the same variants narrows, and the reverse widens. The models that the variants kept, written the same on both sides, refer to are recorded as used there, so a change to one reaches the use; a variant added or removed is judged by the change of variants alone, as no client sent or received it there before, or will after. The keywords beside the variants are compared on their own, with their own flags. A 3.1 union of one schema and {type: null}, which Parse folds into that schema made nullable, is compared as the list of that one variant, under the keyword of the other side's variants, where the other side lists variants and the one schema has none of its own once flattened, so a variant added beside it reads as added, as in the 3.0 spelling, and oneOf: [S, I] to anyOf: [S, {type: null}] reads one of variants removed. Where the one schema has variants of its own, the two are compared as they are, as before, so anyOf: [{$ref: U}, {type: null}], with U a oneOf, to the same list with a variant added reads one of variants changed to any of, breaking both, though it only widens.

The other breaking changes: a removed operation; a removed (or renamed) response property, or a property removed from an object that allows no other properties, or whose additionalProperties schema limits values and is not written as the property's schema was, annotations left out and a $ref the same as another where it names the same component, which breaks a request that still sends it too, also where that schema accepts every value the property's did, as the two are not compared, while where it is written so and the object allowed no other properties before, the models that schema names are used there, so their changes reach the request; a removed response header, or one no longer required; a response property no longer required, which a response may now leave out; a new required request property, parameter or request body; an existing request property, parameter or request body made required, also a name required where no property of that name is declared. A response header named Content-Type is not compared, as OpenAPI says its definition is ignored. Everything else is a non-breaking change.

func (*Report) Breaking

func (r *Report) Breaking() bool

Breaking reports whether any change is breaking.

func (*Report) Empty

func (r *Report) Empty() bool

Empty reports whether the documents have no differences.

func (*Report) Markdown

func (r *Report) Markdown() string

Markdown renders the report as a changelog for release notes or CI: breaking changes first, then the rest, each grouped by the first tag of the operation, with model changes under Models.

func (*Report) Model

func (r *Report) Model(name string) *ModelDiff

Model returns the result for a component schema, or nil when it is the same in both documents.

func (*Report) Operation

func (r *Report) Operation(method, path string) *OperationDiff

Operation returns the result for method and path, or nil when the operation is the same in both documents.

type RequestBody

type RequestBody struct {
	Ref         string              `json:"$ref" yaml:"$ref"`
	Description string              `json:"description" yaml:"description"`
	Required    bool                `json:"required" yaml:"required"`
	Content     Ordered[*MediaType] `json:"content" yaml:"content"`
}

RequestBody is operation.requestBody.

type Response

type Response struct {
	Ref         string              `json:"$ref" yaml:"$ref"`
	Description string              `json:"description" yaml:"description"`
	Headers     Ordered[*Header]    `json:"headers" yaml:"headers"`
	Content     Ordered[*MediaType] `json:"content" yaml:"content"`
}

Response is one status code of operation.responses.

type Schema

type Schema struct {
	Ref           string           `json:"$ref" yaml:"$ref"`
	Title         string           `json:"title" yaml:"title"`
	Description   string           `json:"description" yaml:"description"`
	Type          TypeList         `json:"type" yaml:"type"`
	Format        string           `json:"format" yaml:"format"`
	Nullable      bool             `json:"nullable" yaml:"nullable"`
	NullMember    bool             `json:"-" yaml:"-"`
	NullOnly      bool             `json:"-" yaml:"-"`
	Enum          []any            `json:"enum" yaml:"enum"`
	Const         any              `json:"const" yaml:"const"`
	Default       any              `json:"default" yaml:"default"`
	Example       any              `json:"example" yaml:"example"`
	Examples      []any            `json:"examples" yaml:"examples"`
	Deprecated    bool             `json:"deprecated" yaml:"deprecated"`
	ReadOnly      bool             `json:"readOnly" yaml:"readOnly"`
	WriteOnly     bool             `json:"writeOnly" yaml:"writeOnly"`
	Properties    Ordered[*Schema] `json:"properties" yaml:"properties"`
	Required      []string         `json:"required" yaml:"required"`
	Items         *Schema          `json:"items" yaml:"items"`
	AllOf         []*Schema        `json:"allOf" yaml:"allOf"`
	OneOf         []*Schema        `json:"oneOf" yaml:"oneOf"`
	AnyOf         []*Schema        `json:"anyOf" yaml:"anyOf"`
	Not           *Schema          `json:"not" yaml:"not"`
	Discriminator *Discriminator   `json:"discriminator" yaml:"discriminator"`

	// AdditionalProperties is nil when unset, which JSON Schema reads as
	// allowed. Use AdditionalSchema to get the schema of extra properties.
	AdditionalProperties *AdditionalProperties `json:"additionalProperties" yaml:"additionalProperties"`

	Minimum          *float64 `json:"minimum" yaml:"minimum"`
	Maximum          *float64 `json:"maximum" yaml:"maximum"`
	ExclusiveMinimum *Bound   `json:"exclusiveMinimum" yaml:"exclusiveMinimum"`
	ExclusiveMaximum *Bound   `json:"exclusiveMaximum" yaml:"exclusiveMaximum"`
	MultipleOf       *float64 `json:"multipleOf" yaml:"multipleOf"`
	MinLength        *int     `json:"minLength" yaml:"minLength"`
	MaxLength        *int     `json:"maxLength" yaml:"maxLength"`
	Pattern          string   `json:"pattern" yaml:"pattern"`
	MinItems         *int     `json:"minItems" yaml:"minItems"`
	MaxItems         *int     `json:"maxItems" yaml:"maxItems"`
	UniqueItems      bool     `json:"uniqueItems" yaml:"uniqueItems"`
	MinProperties    *int     `json:"minProperties" yaml:"minProperties"`
	MaxProperties    *int     `json:"maxProperties" yaml:"maxProperties"`

	// Extensions holds the specification extensions (keys starting with
	// "x-") as decoded values, or nil when the schema has none. It is not
	// written back by json.Marshal.
	Extensions map[string]any `json:"-" yaml:"-"`
}

Schema is a JSON Schema as used by OpenAPI 3.0 and 3.1.

Type always holds the non-null types; Nullable is true when 3.0 said nullable: true, when 3.1 listed "null" in the type array, or when a oneOf or anyOf member was {type: "null"}. NullMember is true in the last case only: null is then valid whatever the enum or const say, while the other two add null to the type and leave the enum and const deciding. NullOnly is true when 3.1 listed "null" as the only type, or when the oneOf and anyOf members were all {type: "null"} beside no type, $ref or allOf, which leaves Type empty as for a schema that states no type. Exclusive bounds always hold the numeric form.

func (*Schema) AdditionalSchema

func (s *Schema) AdditionalSchema() *Schema

AdditionalSchema returns the schema for additional properties, or nil when they are a free-form or forbidden.

func (*Schema) IsRequired

func (s *Schema) IsRequired(name string) bool

IsRequired reports whether name is listed in required.

func (*Schema) UnmarshalJSON

func (s *Schema) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts a schema object or a 3.1 boolean schema: true is the empty schema (anything) and false is {"not": {}} (nothing).

func (*Schema) UnmarshalYAML

func (s *Schema) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts a schema mapping or a 3.1 boolean schema.

type SecurityScheme

type SecurityScheme struct {
	Type         string `json:"type" yaml:"type"`
	Description  string `json:"description" yaml:"description"`
	Name         string `json:"name" yaml:"name"`
	In           string `json:"in" yaml:"in"`
	Scheme       string `json:"scheme" yaml:"scheme"`
	BearerFormat string `json:"bearerFormat" yaml:"bearerFormat"`
}

SecurityScheme is one entry of components.securitySchemes.

type Server

type Server struct {
	URL         string                    `json:"url" yaml:"url"`
	Description string                    `json:"description" yaml:"description"`
	Variables   map[string]ServerVariable `json:"variables" yaml:"variables"`
}

Server is one entry of servers.

func (Server) Expand

func (s Server) Expand() string

Expand returns the server URL with every {variable} replaced by its default.

type ServerVariable

type ServerVariable struct {
	Default     string   `json:"default" yaml:"default"`
	Enum        []string `json:"enum" yaml:"enum"`
	Description string   `json:"description" yaml:"description"`
}

ServerVariable is one entry of server.variables.

type Tag

type Tag struct {
	Name        string `json:"name" yaml:"name"`
	Description string `json:"description" yaml:"description"`
	Parent      string `json:"parent" yaml:"parent"`
	XParent     string `json:"x-parent" yaml:"x-parent"`
}

Tag is one entry of tags. Operations are grouped under tags in the sidebar. Parent (OpenAPI 3.2) or, in older documents, the x-parent extension names the tag a child tag nests under; see Document.tagPath.

type TypeList

type TypeList []string

TypeList decodes "type" as either a string (3.0) or an array (3.1).

func (TypeList) Has

func (t TypeList) Has(name string) bool

Has reports whether the list contains name.

func (TypeList) Primary

func (t TypeList) Primary() string

Primary returns the first non-null type, or "" when the schema is untyped.

func (*TypeList) UnmarshalJSON

func (t *TypeList) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts a string or an array of strings. A JSON null leaves the list empty.

func (*TypeList) UnmarshalYAML

func (t *TypeList) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts a scalar or a sequence of scalars.

Jump to

Keyboard shortcuts

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