Skip to content

Repository files navigation

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 .

About

OpenAPI document resolver for libopenapi

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages