openapischema

package module
v0.0.0-...-9d1fe99 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 8 Imported by: 0

README

openapi-schema-go

Small, dependency-light helpers for interpreting an already-parsed libopenapi OpenAPI document: resolving oneOf/anyOf schema unions (including discriminators), reading nullable dialects (3.0 nullable: true and 3.1 anyOf: [T, null]), picking the right response schema for a status code, and resolving/applying apiKey/http security requirements to an outgoing request.

It doesn't parse specs itself — it operates on the model types libopenapi already gives you (*base.Schema, *v3.Operation, *v3.SecurityScheme, ...). Think of it as the "what does this operation actually require, and what shape is this response" layer that most OpenAPI client generators build once and don't expose.

This package has no dependency beyond the Go standard library and libopenapi itself — no reflection, no code generation, nothing framework-specific.

Install

go get github.com/bckground/openapi-schema-go

Schema resolution

ResolveVariant picks the concrete schema for a decoded JSON value, handling three cases: a discriminated oneOf (dispatches on the discriminator property, preferring an explicit mapping entry and falling back to matching a $ref's schema name), a bare oneOf/anyOf union with no discriminator (structural best-match: which candidate's own declared properties overlap the data the most), and a single-candidate or no-candidate schema (returned directly).

document, err := libopenapi.NewDocument(specBytes)
if err != nil {
    return err
}
model, err := document.BuildV3Model()
if err != nil {
    return err
}

petSchema := model.Model.Components.Schemas.Value("Pet").Schema()

var decoded map[string]any
json.Unmarshal(responseBody, &decoded)

// petSchema declares `oneOf: [Dog, Cat]` with a discriminator on `petType`.
resolved, err := openapischema.ResolveVariant(petSchema, decoded)
if err != nil {
    return err
}
// resolved is now the Dog or Cat schema, whichever `decoded` actually is.

Related helpers you'll typically use alongside it:

  • IsNullable(schema) / UnderlyingType(schema) — detect and unwrap both OpenAPI 3.0's nullable: true and 3.1's anyOf: [T, {type: null}].
  • IsArraySchema(schema) — whether a schema declares the JSON Schema array type.
  • IsFreeForm(schema) — whether a schema is unstructured (no properties, no composition keywords) and should be treated as an opaque passthrough rather than resolved further.
  • PropertySchema(schema, name) — the schema for a named property, checking the schema's own properties first and then each allOf member in order.

Response schema selection

ResponseSchema finds the declared schema for an operation's response, given the actual status code received: an exact match in op.Responses.Codes, falling back to op.Responses.Default, then the first media type whose content-type contains "json".

op := findOperation(model, "/pets/{id}", "get") // however you look it up

schema, ok := openapischema.ResponseSchema(op, resp.StatusCode)
if !ok {
    // Nothing declared for this status/content-type - handle the raw body yourself.
}
// schema is a *base.Schema you can now decode `resp.Body` against.

FirstJSONSchema is the lower-level piece ResponseSchema is built on, useful directly if you're resolving a request body's schema instead of a response's — both op.RequestBody.Content and a response's Content are the same *orderedmap.Map[string, *v3.MediaType] shape.

Security scheme resolution

ResolveOperationSecurity determines the effective, AND'd set of security schemes an operation requires — honoring OpenAPI's "operation-level security entirely overrides the document's global security, even when declared as an empty array" rule. ApplySecurity then injects the corresponding credentials into an *http.Request.

Supported today: apiKey (header or query, not cookie) and http (basic or bearer). Anything else — oauth2, openIdConnect, cookie-based apiKey, other http schemes, or more than one alternative requirement in the security array — is rejected with an error rather than silently picked or ignored.

var securitySchemes *orderedmap.Map[string, *v3.SecurityScheme]
if model.Model.Components != nil {
    securitySchemes = model.Model.Components.SecuritySchemes
}

resolved, err := openapischema.ResolveOperationSecurity(
    securitySchemes,
    model.Model.Security, // document-level default
    op.Security,           // this operation's own requirement, if any
)
if err != nil {
    return err // e.g. "security scheme \"oauth2Auth\" has unsupported type..."
}

req, _ := http.NewRequest(http.MethodGet, url, nil)
credentials := map[string]string{"apiKeyAuth": "secret-value"}
if err := openapischema.ApplySecurity(req, resolved, credentials); err != nil {
    return err // e.g. "missing credential for security scheme \"apiKeyAuth\""
}

MergeSecurity(base, override) overlays a smaller, call-specific credentials map onto a larger default one — useful if your caller lets a single request override just the schemes it cares about:

credentials := openapischema.MergeSecurity(clientDefaultCredentials, perCallOverride)

Testing

go test ./...

or, with Ginkgo:

ginkgo --race --fail-fast --keep-going --randomize-all --randomize-suites --fail-on-empty --require-suite .

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplySecurity

func ApplySecurity(req *http.Request, schemes []ResolvedSecurityScheme, credentials map[string]string) error

ApplySecurity injects credentials into the outgoing request for each resolved scheme, using the caller-supplied credentials map (keyed by scheme name). A missing credential for a scheme the invoked operation actually requires fails here, at call time, using only the standard library's net/http and encoding/base64 - not eagerly at earlier construction time, since an operation the caller never calls shouldn't require its credentials to be supplied.

This returns a plain Go error. Callers embedding this in a language runtime with its own catchable/uncatchable error distinction (e.g. a scripting layer) should treat a missing credential as a caller bug - not a recoverable runtime condition like an HTTP-level auth failure from the server would be - and propagate it accordingly.

func FirstJSONSchema

func FirstJSONSchema(content *orderedmap.Map[string, *v3.MediaType]) (*base.Schema, bool)

FirstJSONSchema finds the schema of the first JSON media type in content (an operation's requestBody.content or a response's content - both the same *orderedmap.Map[string, *v3.MediaType] shape), skipping any content-type not containing "json" and any media type with no schema. Returns ok=false if content is nil or declares no matching media type.

func IsArraySchema

func IsArraySchema(schema *base.Schema) bool

IsArraySchema reports whether schema declares the JSON Schema "array" type.

func IsFreeForm

func IsFreeForm(schema *base.Schema) bool

IsFreeForm reports whether schema describes an unstructured/arbitrary JSON value - no fixed Properties, and no composition (allOf/oneOf/anyOf) that would let the value's shape be resolved. A caller decoding a free-form value should fall back to a generic, untyped conversion rather than a schema-driven one - this is the right behavior for something like a tool's free-form `input_schema`, which must stay an opaque passthrough.

func IsNullable

func IsNullable(schema *base.Schema) bool

IsNullable reports whether schema allows a JSON null value, across both OpenAPI 3.0 (`nullable: true`) and 3.1 (`anyOf: [T, {type: null}]`) dialects.

func MergeSecurity

func MergeSecurity(base, override map[string]string) map[string]string

MergeSecurity overlays call-level credential overrides onto a base credentials map, so a call can override just the schemes it cares about without having to resupply every credential the base map already has.

func PropertySchema

func PropertySchema(schema *base.Schema, name string) (*base.Schema, bool)

PropertySchema resolves the schema for a named property, checking the schema's own Properties first, then each AllOf member in order - a merged property view, since the schema doesn't compose AllOf members into one and resolution has to check each in turn.

func ResolveVariant

func ResolveVariant(schema *base.Schema, data map[string]any) (*base.Schema, error)

ResolveVariant determines the concrete schema for a value at this node, given the schema declared for it and the already-decoded data. If schema is a discriminated oneOf, it dispatches on the discriminator property's value in data. If it's a bare oneOf/anyOf union (no discriminator) with more than one candidate, it falls back to structural matching. A union with exactly one candidate resolves to that candidate directly - there's nothing to disambiguate, and the wrapper schema itself has no Properties of its own to decode against. A schema with no candidates at all (neither oneOf/anyOf, or an anyOf that's only the nullable-null branch) is returned unchanged.

func ResponseSchema

func ResponseSchema(op *v3.Operation, statusCode int) (*base.Schema, bool)

ResponseSchema finds the declared response schema for an operation given the actual received status code: an exact match in op.Responses.Codes (keyed by the status code string, e.g. "200"), falling back to op.Responses.Default. Returns ok=false if the operation declares no response schema at all for this status - callers should preserve whatever their pre-typed-response behavior was in that case.

func UnderlyingType

func UnderlyingType(schema *base.Schema) *base.Schema

UnderlyingType returns the non-null member of a nullable anyOf (OpenAPI 3.1's `anyOf: [T, {type: null}]` idiom), or schema itself if it isn't that pattern.

Types

type ResolvedSecurityScheme

type ResolvedSecurityScheme struct {
	// contains filtered or unexported fields
}

ResolvedSecurityScheme is a single scheme (from one AND'd security requirement) that must be satisfied to call an operation.

func ResolveOperationSecurity

func ResolveOperationSecurity(
	securitySchemes *orderedmap.Map[string, *v3.SecurityScheme],
	globalSecurity []*base.SecurityRequirement,
	opSecurity []*base.SecurityRequirement,
) ([]ResolvedSecurityScheme, error)

ResolveOperationSecurity determines the effective, AND'd set of security schemes required for an operation. The operation's own `security` (if declared at all, even as an empty slice) entirely overrides the document's global `security` - the OpenAPI "operation-level takes precedence" rule, matching libopenapi's own nil-vs-empty-slice distinction (v3/operation.go's Operation.Security: a nil Security means "not declared", an empty-but-non-nil slice means "declared as empty", i.e. explicitly no auth for this operation).

OR-alternatives (more than one requirement in the effective array) are out of scope for this iteration and fail loudly rather than silently picking one; so do any scheme types other than apiKey/http, cookie-based apiKey, and http schemes other than basic/bearer.

Jump to

Keyboard shortcuts

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