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.
go get github.com/bckground/openapi-schema-go
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'snullable: trueand 3.1'sanyOf: [T, {type: null}].IsArraySchema(schema)— whether a schema declares the JSON Schemaarraytype.IsFreeForm(schema)— whether a schema is unstructured (noproperties, 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 ownpropertiesfirst and then eachallOfmember in order.
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.
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)go test ./...
or, with Ginkgo:
ginkgo --race --fail-fast --keep-going --randomize-all --randomize-suites --fail-on-empty --require-suite .