Skip to content

parser: $ref-form Callback Objects fail to decode #451

Description

@erraggy

Epic A — Correct the measured conformance defects · Size M · Release v1.60.0 · Blocked by: nothing

The second half of #447. The boolean-schema half landed in #450; this is the part that needed a different approach.

The defect

A Callback Object may be a Reference Object. oastools cannot decode that form, and rejects the entire document:

callbacks:
  myCallback:
    '{$request.query.queryUrl}':      # runtime expression
      post: { ... }
  transactionCallback:
    $ref: '#/components/callbacks/transactionCallback'   # a Reference
line 45: cannot construct !!str `#/compo...` into parser.PathItem

Reproduces on tests/schema/pass/path_item_servers_parameters.yaml from OAI/OpenAPI-Specification@v3.2-dev. It is the last remaining positive-fixture failure not owned by #434.

Why this one is structurally awkward

OAS 3.2 has nine -or-reference unions: parameter, request-body, media-type, response, callbacks, example, link, header, security-scheme.

Eight are objects with fixed field names, so oastools models each as a struct with a Ref string field — $ref cannot collide with name, schema, description. Link, Response and Parameter all do exactly this.

The Callback Object is the outlier: an open map keyed by user-authored runtime expressions, so $ref shares a key namespace with them. The spec discriminates on key presence (schema.yaml:737):

callbacks-or-reference:
  if:   { type: object, required: [$ref] }
  then: { $ref: '#/$defs/reference' }
  else: { $ref: '#/$defs/callbacks' }

In Go:

// parser/paths.go:132
type Callback map[string]*PathItem

A named map type has methods but no fields, so there is nowhere to put Ref. The parser hands $ref to the map as if it were a runtime expression and fails building a PathItem from the string.

Options examined

Option Additive? Verdict
type Callback struct { Ref string; Expressions map[string]*PathItem } Correct model. Breaks walker.CallbackHandler (walker/walker.go:117) — a public signature users implement — plus validator/oas32_gate.go:321, joiner and fixer. Needs a major version
Reserved "$ref" key inside the existing map ✓ compiles Rejected. Leaks a key that is not a runtime expression into every consumer that ranges the map, third-party handlers included. Silent corruption is worse than a parse error
Union at the container: map[string]*CallbackOrReference Same breakage relocated; the field types on Operation and Components are public
Parallel field Chosen — see below
Resolve/inline the $ref at parse time Rejected — lossy, and references are preserved verbatim for lossless round-trips

Approach: a parallel field

Add CallbackRefs map[string]*Reference beside the existing Callbacks map[string]*Callback on both Operation (parser/paths.go:46) and Components (parser/oas3.go:41), tagged yaml:"-" json:"-", and merge both back into a single callbacks: object when serializing. parser.Reference already exists (parser/common.go:88) with Ref/Summary/Description, so the value type is off the shelf.

  • Buys: parses, round-trips losslessly, no public signature change.
  • Costs: one specification field represented by two Go fields — an invariant the parser must police, since a key must not appear in both. Consumers reading only Callbacks will miss referenced ones; today they cannot parse the document at all, so it is strictly an improvement.

Acceptance

  • pass/path_item_servers_parameters.yaml parses and validates clean; the reference round-trips verbatim in both YAML and JSON.
  • All three decode paths handle it — YAML, JSON, and the generated decodeFromMap. feat(parser,validator): support bare-boolean schemas for OAS 3.1+ #450 found the third silently dropping values rather than erroring; that trap is live here too.
  • The same key cannot appear in both maps; a document attempting it is rejected rather than silently favouring one.
  • Deep copy, equality and the driftguard entries cover the new field. feat(parser,validator): support bare-boolean schemas for OAS 3.1+ #450 found the deepcopy generator's hand-maintained field list silently omits new fields.
  • walker gains an additive handler if references should be walkable; existing CallbackHandler behaviour is unchanged.

Context

Callbacks are rare in practice — none of the ten real-world specs in the corpus use them, and only two fixtures in the OAI 3.2 suite mention them. The cost of the gap is not frequency but blast radius: a document that merely contains one referenced callback fails to parse entirely, rather than degrading.

Files

parser/paths.go, parser/oas3.go, parser/common.go, internal/codegen/decode/, internal/codegen/deepcopy/, internal/driftguard/, walker/


Context: design doc · plan doc

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingconformanceOpenAPI specification conformance workoas3.xIssues related to OpenAPI Specification versions 3+

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions