Skip to content

Initial OpenAPI 3.1 support based on kin-openapi#2336

Merged
mromaszewicz merged 33 commits into
mainfrom
feat/kin-openapi-3.1
Jun 20, 2026
Merged

Initial OpenAPI 3.1 support based on kin-openapi#2336
mromaszewicz merged 33 commits into
mainfrom
feat/kin-openapi-3.1

Conversation

@mromaszewicz

@mromaszewicz mromaszewicz commented Apr 25, 2026

Copy link
Copy Markdown
Member

Fixes: #373

This is a back-port of my OpenAPI 3.1 work in the oapi-codegen-exp repo.

Since kin-openapi now requires Go 1.26.2, merging this requires some updates to our build, and also increasing our Go version dependency. That will require a bit of cleanup in our sub-modules and lots of them can be simplified. For now, I've done the minimal set of work in this branch to get it to compile.

OpenAPI 3.1 idioms suported:

  • webhooks
  • callbacks (in 3.0 too)
  • consts
  • enum via allOf aggregates for better customization.

@mromaszewicz mromaszewicz added enhancement New feature or request notable changes Used for release notes to highlight these more highly 3.1 labels Apr 25, 2026
@mromaszewicz

Copy link
Copy Markdown
Member Author

@greptileai Please review this large change. It's a back-port from a different repo where this functionality is well tested. It's not ready to merge, due to kin-openapi not having a release ready, but I'd like feedback on the work to date to see if I missed anything major. Pay extra attention to places where OpenAPI 3.0 and OpenAPI 3.1 schemas provide alternative paths in the codegen.

@greptile-apps

greptile-apps Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds initial OpenAPI 3.1 support to the generator. The main changes are:

  • Support for OpenAPI 3.1 webhooks and callbacks in generated clients and servers.
  • Schema handling for 3.1 nullable forms, consts, content keywords, and enum-via-oneOf patterns.
  • New examples and internal fixtures covering callbacks, webhooks, nullable schemas, and content encoding/media type behavior.
  • Updated generated outputs and configuration schema entries for the new options.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Reviews (6): Last reviewed commit: "codegen: only map contentEncoding to []b..." | Re-trigger Greptile

Comment thread go.mod
@mromaszewicz mromaszewicz added this to the v2.8.0 milestone Apr 25, 2026
@mromaszewicz
mromaszewicz force-pushed the feat/kin-openapi-3.1 branch from e4b5380 to d33792a Compare April 25, 2026 13:57
@mromaszewicz

Copy link
Copy Markdown
Member Author

@greptileai - all comments addressed, review again.

@mromaszewicz
mromaszewicz marked this pull request as ready for review April 25, 2026 14:06
@mromaszewicz
mromaszewicz requested a review from a team as a code owner April 25, 2026 14:06
@jamietanna

Copy link
Copy Markdown
Member

(note to folks following this - we'll be releasing this as a 2.8.0 release, after 2.7.0)

@jamietanna

Copy link
Copy Markdown
Member

@jamietanna

Copy link
Copy Markdown
Member

Worth checking if anything from 620da6b (and the related change to make the Type an array of types needs explicit handling

Comment thread pkg/codegen/configuration.go

@jamietanna jamietanna left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also need a README change when this is closer to completion - to note that we do support 3.1 (but not 3.2!)

@jamietanna jamietanna left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have a more in-depth review shortly, but so far this is looking reasonable 🥳

@mromaszewicz

Copy link
Copy Markdown
Member Author

Do we think this will close any others from https://github.com/oapi-codegen/oapi-codegen/issues?q=is%3Aissue%20state%3Aopen%20openapi%203.1%20label%3A3.1 ?

Yes. I will add them to PR description.

@mromaszewicz
mromaszewicz force-pushed the feat/kin-openapi-3.1 branch from d33792a to 38df03d Compare April 30, 2026 17:52
@mromaszewicz

Copy link
Copy Markdown
Member Author

I rebased this onto the latest main, and downgraded the Go requirement to 1.25.9

mromaszewicz and others added 9 commits May 3, 2026 19:57
kin-openapi's 3.1 work changed Schema.ExclusiveMin/Max from bool to
ExclusiveBound{Bool *bool; Value *float64}. Direct struct equality (!=)
still compiles but compares pointer addresses, not the wrapped values, so
two schemas with semantically identical bounds would incorrectly fail the
merge. Use reflect.DeepEqual for value-aware comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is Phase 1 of bringing 3.1 feature parity from oapi-codegen-exp into
mainline. It establishes the architectural patterns the rest of the work
will follow.

Version awareness:
* New globalState.is31 flag, populated once at Generate() entry from
  swagger.IsOpenAPI31OrLater(). All version-aware logic lives in Go and
  branches on this flag; templates remain version-blind and consume
  pre-computed derived fields.
* Comment on the field explicitly forbids exposing it to TemplateFunctions
  to prevent layering violations.

Nullable handling:
* New schemaIsNullable() helper that branches on globalState.is31:
  3.0 reads s.Nullable; 3.1 reads s.Type.Includes("null").
* New schemaPrimaryType() helper strips "null" from the type slice in
  3.1 mode so existing dispatch (`*Types.Is("string")` etc.) keeps
  working for type:["string","null"].
* Four direct .Nullable read sites switched to the helper:
  schema.go (property construction, array nullable, additionalProperties)
  and operations.go (response header).
* merge_schemas.go's Nullable comparison routed through the helper; an
  explanatory comment notes that type merging itself is NOT version
  branched (equalTypes() handles single-element 3.0 and multi-element
  3.1 type slices identically), and that result.Type already carries
  any "null" entry forward in 3.1 mode.

Other:
* Adds OutputOptions.SkipEnumViaOneOf for Phase 2 (enum-via-oneOf
  detection); declared now to keep the config schema stable across
  phases.
* Removes the stale "OpenAPI 3.1.x is not yet supported" stderr
  warning at cmd/oapi-codegen entry; partial 3.1 support now exists
  and the message will become misleading as later phases land.
* Two path parameters in pkg/codegen/test_specs/x-go-type-import-pet.yaml
  were missing the OpenAPI-required `required: true`. The omission was
  incidental to what the test exercises (x-go-type-import); the spec is
  now spec-conformant.

Tests:
* internal/test/openapi31_nullable/ -- two specs (3.0 nullable:true,
  3.1 type:["string","null"]) generate into spec_3_0/ and spec_3_1/
  subpackages. Instantiation tests (no string-matching) assert both
  produce *string and round-trip JSON identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the 3.1 backport. Detects the 3.1 enum-via-oneOf idiom and
routes it through the existing typed-enum codegen path so users see the
same Go shape they'd get from a plain `enum:` array.

The idiom (per OpenAPI 3.1 / JSON Schema 2020-12):

    Severity:
      type: integer        # or "string"
      oneOf:
        - title: HIGH
          const: 2
          description: An urgent problem
        - title: MEDIUM
          const: 1
        - title: LOW
          const: 0

Generates:

    type Severity int
    const (
        HIGH   Severity = 2
        LOW    Severity = 0
        MEDIUM Severity = 1
    )

Implementation:
* New detectEnumViaOneOf() helper in pkg/codegen/schema.go. Trigger
  conditions: globalState.is31 is true, !SkipEnumViaOneOf, schema's
  primary type is "string" or "integer", every oneOf branch carries
  non-empty Title AND non-nil Const, and no branch is itself a
  composition or has Properties.
* GenerateGoSchema short-circuits between the AllOf branch and the
  type dispatch: when the idiom matches, populate Schema.EnumValues
  directly and register an AdditionalType for non-toplevel schemas
  so the existing EnumDefinition collector picks them up. Negative
  matches fall through to the standard oneOf union generator.
* No template changes -- the existing constants.tmpl renders the
  typed enum + Valid() method via the standard EnumDefinition path.
* The OutputOptions.SkipEnumViaOneOf flag declared in Phase 1 is
  now actually consulted here.

Tests:
* internal/test/enum_via_oneof/ -- ports the exp test fixture
  (Severity int, Color string, MixedOneOf negative path). Five
  instantiation tests (no string-matching) cover constant values,
  JSON round-trips, and the negative path's compile-time alias
  shape (a plain string is directly assignable to MixedOneOf,
  proving it did NOT become a typed-enum newtype).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iver

Phase 3 of the 3.1 backport. Generates client-side and server-side
support for `webhooks:` declared in OpenAPI 3.1 specs. The output
mirrors the existing Client / Server interfaces in shape but lives in
its own types, matching the conventions oapi-codegen-exp established.

Data model:
* OperationDefinition gains IsWebhook bool and WebhookName string.
* New WebhookOperationDefinitions() walks swagger.Webhooks and produces
  OperationDefinitions in the same shape as path operations, minus the
  path-alias and path-parameter logic (webhooks have no path template).
  No version gate -- kin-openapi only populates the Webhooks field for
  3.1+ documents, so 3.0 specs short-circuit on the empty map.

Generated client (WebhookInitiator):
* New pkg/codegen/templates/webhook-initiator.tmpl, modeled on
  client.tmpl. Emits:
    type WebhookInitiator struct{ Client; RequestEditors }
    NewWebhookInitiator + WebhookInitiatorOption + WithWebhookHTTPClient
        + WithWebhookRequestEditorFn
    WebhookInitiatorInterface
    per-webhook methods (Op + OpWithBody) and request builders
        (NewOpWebhookRequest + NewOpWebhookRequestWithBody)
* No stored Server -- the target URL is supplied per-call by the
  caller (typically discovered from a subscription registration).
* Gated by Generate.Client (paired with the path Client).

Generated server (WebhookReceiver, stdhttp):
* New pkg/codegen/templates/stdhttp/std-http-webhook-receiver.tmpl.
  Emits:
    type WebhookReceiverInterface { HandleOpWebhook(w, r) for each }
    type WebhookReceiverMiddlewareFunc func(http.Handler) http.Handler
    OpWebhookHandler(si, middlewares...) http.Handler -- per-webhook
        factory; caller mounts the returned http.Handler at the URL
        path they advertise to subscribers.
* Methods take plain (w, r) -- the user reads/parses the body
  themselves. Simpler than the full ServerInterfaceWrapper machinery;
  parameter binding can be added later if the demand surfaces.
* Gated by Generate.StdHTTPServer (paired with the path stdhttp
  ServerInterface). Other frameworks are left as follow-ups.

Wiring (codegen.go):
* allOps = ops + webhookOps is passed to OperationImports and
  GenerateTypeDefinitions so webhook bodies and responses generate
  their type definitions and imports normally.
* New GenerateWebhookInitiator and GenerateStdHTTPWebhookReceiver entry
  points in operations.go, modeled on GenerateClient and
  GenerateStdHTTPServer.
* Output ordering: webhook initiator follows ClientWithResponses;
  webhook receiver follows the path StdHTTPServer block.

Tests (internal/test/webhooks/):
* spec.yaml declares a single petStatusChanged webhook with a JSON
  PetStatusEvent body and a 204 response.
* TestWebhookRoundTrip mounts the generated factory against an
  httptest.Server and fires a webhook via the initiator; asserts the
  payload, method, and Content-Type round-trip intact.
* TestWebhookInitiatorRequestEditor verifies WithWebhookRequestEditorFn
  applies on every outgoing request (parity with the path Client).
* TestWebhookReceiverMiddleware documents middleware composition order:
  the factory wraps in `for _, mw := range middlewares { h = mw(h) }`,
  so the LAST middleware passed becomes the outermost wrapper.

Example (examples/webhook/):
* Door badge reader scenario ported from oapi-codegen-exp/examples/webhook.
  The server randomly generates enter/exit badge events every second
  and fires the appropriate webhook to all registered subscribers; the
  client subscribes to both event kinds via the path API, runs a local
  HTTP receiver to handle inbound events, and deregisters cleanly on
  exit. Demonstrates the full Client/Server + WebhookInitiator/Receiver
  pairing in one example.
* Lives as a regular package under examples/go.mod -- no separate
  module, no tools.go shim. `make generate` from examples/ regenerates
  via the package's go:generate directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 of the 3.1 backport. Generates client-side and server-side
support for `callbacks:` blocks nested under path operations. The output
mirrors WebhookInitiator / WebhookReceiver in shape -- separate types
named with the `Callback` prefix to match oapi-codegen-exp.

Callbacks have been part of the OpenAPI spec since 3.0, so this is NOT
version-gated: any spec that declares callbacks gets them generated.

Data model:
* OperationDefinition gains IsCallback bool and CallbackName string
  (alongside the IsWebhook / WebhookName pair from Phase 3).
* New CallbackOperationDefinitions() walks
  spec.Paths.<path>.<method>.Callbacks. Each leaf operation under the
  callback's URL-expression key becomes one OperationDefinition with
  IsCallback=true and CallbackName set to the outer map key (e.g.
  "treePlanted"). The codegen does not interpret the URL expression
  itself -- the caller of CallbackInitiator supplies the resolved
  target URL at runtime, the same way it does for a webhook.
* Callback's internal map is private; iterate via cb.Keys() / cb.Value()
  with sorted keys for deterministic output.

Generated client (CallbackInitiator):
* New pkg/codegen/templates/callback-initiator.tmpl. Same shape as the
  Phase 3 webhook-initiator.tmpl with `Webhook` -> `Callback` rename:
    type CallbackInitiator struct{ Client; RequestEditors }
    NewCallbackInitiator + CallbackInitiatorOption
        + WithCallbackHTTPClient + WithCallbackRequestEditorFn
    CallbackInitiatorInterface
    per-callback methods (Op + OpWithBody) and request builders
        (NewOpCallbackRequest + NewOpCallbackRequestWithBody)
* No stored Server -- targetURL is per-call.
* Gated only by Generate.Client + non-empty callbackOps.

Generated server (CallbackReceiver, stdhttp):
* New pkg/codegen/templates/stdhttp/std-http-callback-receiver.tmpl.
* WebhookReceiverInterface analog: Handle{Op}Callback(w, r) plus a
  per-callback factory function {Op}CallbackHandler(si, middlewares...)
  http.Handler.
* Gated only by Generate.StdHTTPServer + non-empty callbackOps.

Wiring (codegen.go):
* allOps is now `ops + webhookOps + callbackOps` so type-definition
  and import passes see callback bodies / responses.
* Callback initiator output follows webhook initiator (so the order is
  Client / ClientWithResponses / WebhookInitiator / CallbackInitiator).
* Callback receiver output follows webhook receiver under the
  StdHTTPServer block.
* New GenerateCallbackInitiator and GenerateStdHTTPCallbackReceiver
  template entry points in operations.go.

Tests (internal/test/callbacks/):
* spec.yaml deliberately uses `openapi: 3.0.3` to verify the codegen
  does NOT gate callback emission on 3.1 (callbacks are 3.0+).
  Models a tree-planting flow: parent operation PlantTree, callback
  treePlanted with TreePlantingResult body.
* TestCallbackRoundTrip mounts the generated factory against an
  httptest.Server and fires a callback via the initiator; asserts the
  payload, method, and Content-Type round-trip intact.
* TestCallbackInitiatorRequestEditor verifies WithCallbackRequestEditorFn
  applies on every outgoing request (parity with the path Client and
  the WebhookInitiator).

Example (examples/callback/):
* Tree farm scenario ported from oapi-codegen-exp/examples/callback.
  Server accepts PlantTree, returns 202, sleeps 1-5s, then fires the
  treePlanted callback to the caller-supplied URL. Client opens an
  ephemeral receiver, fires 10 plantings in sequence, and waits on a
  channel until all 10 callbacks have arrived.
* Lives as a regular package under examples/go.mod -- no separate
  module, no tools.go shim. Same flat layout as examples/webhook/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 of the 3.1 backport. Two small additive features that round
out the 3.1 surface; no template changes.

`examples:` (plural array) → Go doc comments:
* New describeWithExamples() helper in pkg/codegen/schema.go folds
  example data into the description string used for generated doc
  comments. Version-aware: 3.0 reads schema.Example (singular), 3.1
  reads schema.Examples (plural array). Cross-version misuse is
  documented as invalid input and the helper does not look at the
  off-version field.
* Non-string examples are JSON-encoded so structured values render
  readably; strings round-trip as themselves.
* Applied at the three sites where Description flows from the spec
  schema into the generated Schema/Property: GenerateGoSchema's $ref
  short-circuit, the main outSchema construction, and per-property
  population during object walk.

`const:` → typed alias + singleton constant:
* The existing enum branch in GenerateGoSchema now also fires on
  `(globalState.is31 && schema.Const != nil)`. A local enumSource
  abstracts over the two cases so a scalar `const: X` schema emits the
  same shape as `enum: [X]`: a typed alias plus a sanitized constant
  derived from the value.
* Falls through the standard EnumDefinition pipeline -- existing
  constants.tmpl renders the typed enum + Valid() method without
  changes.

Paths optional in 3.1: no-op. Every swagger.Paths access site
(filter.go, prune.go, gather.go, operations.go, the new
WebhookOperationDefinitions and CallbackOperationDefinitions) already
nil-guards.

Tests (internal/test/openapi31_polish/):
* Pet.Name has a description plus two string examples; Pet.Lives has
  one integer example. Status is a top-level `const: active` schema.
* TestStatusConstSchema (instantiation): `var s Status = Active`
  compiles only because Status is a distinct named type, and asserts
  the constant's value plus that Valid() returns true.
* TestPetExampleComments parses the generated Go source and walks the
  AST to extract Pet's per-field doc comments, then asserts the
  expected description and "Examples: ..." fragments. Doc comments
  aren't runtime-introspectable, so AST inspection of the generated
  file is the right granularity here -- not a brittle string-match
  against the file as a whole.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…add param binding

Phase 6.1 of the 3.1 backport (kicking off the per-framework receiver
work). Sets up the prototype shape that the other 7 server-framework
receivers will follow.

Two changes that always have to land together:

1. Unify the two receiver templates into one parameterized template,
   matching oapi-codegen-exp's structure:
   * Replaces pkg/codegen/templates/stdhttp/std-http-webhook-receiver.tmpl
     and pkg/codegen/templates/stdhttp/std-http-callback-receiver.tmpl
     (deleted) with a single std-http-receiver.tmpl. The new template
     takes a Prefix data field ("Webhook" or "Callback") and emits the
     correct interface, middleware type, and per-op handler factory.
   * New ReceiverTemplateData struct + NewReceiverTemplateData
     constructor in operations.go feed the template.
   * Single GenerateStdHTTPReceiver(t, prefix, ops) entry point;
     callers in Generate() pass "Webhook" or "Callback".

2. Add query/header parameter binding to the receiver factory,
   matching mainline's existing path-server middleware pattern.
   The factory now takes an errHandler argument:

       func PetStatusChangedWebhookHandler(
           si WebhookReceiverInterface,
           errHandler func(w http.ResponseWriter, r *http.Request, err error),
           middlewares ...WebhookReceiverMiddlewareFunc,
       ) http.Handler

   When operations have query/header params (rare for webhooks but
   common for callbacks), the inline closure binds them into a typed
   {Op}Params struct via runtime.BindQueryParameterWithOptions /
   runtime.BindStyledParameterWithOptions and routes any error through
   errHandler. errHandler may be nil; the default returns 400 with the
   error message. When operations have no params, errHandler is
   unused -- pass nil and the prior shape's behavior is preserved.

   This is a breaking change to anyone using the Phase 3/4 generated
   *Handler(si, middlewares...) shape. Acceptable since the Phase 3/4
   surface is unreleased and only-this-branch.

New helper: OperationDefinition.SourceName() returns WebhookName when
IsWebhook, CallbackName when IsCallback. Templates use it to label
emitted handlers uniformly without branching on the source kind.

Existing call sites updated to pass nil for errHandler:
  internal/test/webhooks/webhooks_test.go      (3 places)
  internal/test/callbacks/callbacks_test.go    (2 places)
  examples/webhook/client/main.go              (2 places)
  examples/callback/client/main.go             (1 place)

The four affected gen files are regenerated. Other gen files in the
tree have unrelated drift from Phase 5's examples-in-doc-comments
feature (those don't touch the new receiver shape); a follow-up
`make generate` commit picks them up cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…omments

Phase 5 (eedf057) added the describeWithExamples() helper that folds
schema.Example / schema.Examples into the description string used for
generated Go doc comments. Existing test specs that already had
`example:` set on their schemas now produce richer doc comments.

This commit is purely the result of running `make generate` -- no
behavior change, only doc-comment additions like:

    // Code The underlying http status code
    +//
    +// Example: 500
    Code int32 `json:"code"`

Touched files:
  examples/minimal-server/stdhttp/api/ping.gen.go
  internal/test/externalref/petstore/externalref.gen.go
  internal/test/issues/issue-1087/deps/deps.gen.go
  internal/test/issues/issue-1168/api.gen.go
  internal/test/issues/issue1561/issue1561.gen.go

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 6.2 of the per-framework receiver work. Chi's path-handler
signature is identical to stdhttp's (`(w http.ResponseWriter, r
*http.Request)`), so the receiver template is structurally identical;
only the file path and Go entry point are new.

* New pkg/codegen/templates/chi/chi-receiver.tmpl -- a copy of the
  Phase 6.1 unified stdhttp receiver template. Single template handles
  both webhook and callback receivers via the Prefix data field.
* New GenerateChiReceiver() entry point in operations.go.
* In Generate(): two new blocks gated on Generate.ChiServer + non-empty
  webhook/callback ops, mirroring the stdhttp blocks. Generated output
  is written inside the existing chi-server WriteString block so all
  chi-related output stays grouped.
* New internal/test/webhooks_chi/ -- compile-time assertion that the
  chi receiver template produces valid Go. The runtime round-trip
  behavior is already covered by internal/test/webhooks (stdhttp);
  since chi shares the signature, that coverage transfers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mromaszewicz and others added 8 commits May 3, 2026 19:57
…/<framework>/

Final Phase 6 cleanup. Reorganizes the seven flat per-framework
webhook test directories under a single internal/test/webhooks/
parent so they group together and the layout reflects the
framework-fan-out done in Phases 6.1-6.8.

Layout change:
  internal/test/webhooks/                 (the original stdhttp tests)
  internal/test/webhooks_chi/             }
  internal/test/webhooks_echo/            }
  internal/test/webhooks_fiber/           }  flat per-framework dirs
  internal/test/webhooks_gin/             }
  internal/test/webhooks_gorilla/         }
  internal/test/webhooks_iris/            }

becomes:

  internal/test/webhooks/
  ├── spec.yaml          (shared by all subdirs)
  ├── stdhttp/           (original round-trip tests)
  ├── chi/
  ├── echo/
  ├── fiber/
  ├── gin/
  ├── gorilla/
  └── iris/

Per-subdir changes:
* Package name simplified from `webhooks` / `webhooks_<framework>` to
  just `<framework>` (matches dir name; package-vs-import name overlap
  with framework packages like chi/v5 is harmless since Go's local
  package name is implicit).
* Generated file uniformly named `webhooks.gen.go` (was
  `webhooks_<framework>.gen.go` in flat dirs).
* config.yaml `package:` and `output:` directives updated; schema
  reference path bumped one level deeper.
* doc.go `package` and prose updated; `go:generate` now points at the
  shared `../spec.yaml`.

Spec deduplication:
* The seven copies of spec.yaml (functionally identical, only
  title/description varied) collapse to one shared file at
  internal/test/webhooks/spec.yaml. Each subdir's go:generate
  references it via `../spec.yaml`.

The round-trip test in stdhttp/webhooks_test.go has its package
declaration updated to `package stdhttp`.

No code or template changes outside the moves; same gen output up to
package name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from automated review:

P1 -- Non-deterministic map iteration in CallbackOperationDefinitions
(pkg/codegen/operations.go).

The inner loop walked `pathItem.Operations()` directly. When a path
declares callbacks on multiple HTTP methods (e.g. both POST and PUT),
the generated output would differ between runs because Go map
iteration is randomized. Fix: copy keys into a slice and walk via
SortedMapKeys, matching the pattern used by every other similar loop
in the codebase (including WebhookOperationDefinitions immediately
above).

P2 -- Hidden global-state dependency in version-aware helpers
(pkg/codegen/schema.go).

schemaIsNullable, schemaPrimaryType, describeWithExamples, and
detectEnumViaOneOf all read globalState.is31 implicitly, with no
indication at the function signature that callers must invoke them
only after Generate() has initialized the global. Fix: add an
explicit "Precondition" line to each helper's doc comment so callers
(including future refactors that move schema resolution earlier) are
aware of the dependency. The signatures themselves are not changed --
threading the version flag through every call site would be an
intrusive refactor for a problem better described in prose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in the official kin release which adds OpenAPI 3.1 support, and
updates CI to run only on Go 1.26
Two distinct lint issues across four test files.

errcheck (5 sites): defer / direct calls to r.Body.Close() and
resp.Body.Close() ignored their error returns. Standard fix:
* `defer r.Body.Close()` -> `defer func() { _ = r.Body.Close() }()`
* `resp.Body.Close()` (non-deferred) -> `_ = resp.Body.Close()`

  internal/test/callbacks/callbacks_test.go         (1)
  internal/test/webhooks/stdhttp/webhooks_test.go   (4)

ST1023 (2 sites): "should omit type X from declaration; it will be
inferred from the right-hand side". Two different stories.

* internal/test/openapi31_polish/openapi31_polish_test.go: `var s
  Status = Active` is genuinely redundant -- `Active` is declared
  `const Active Status = "active"`, so type inference gives `s` the
  type `Status` already. Switched to `s := Active`. Comment updated
  to explain the still-meaningful compile-time check (if Active had
  been emitted untyped, `s := Active` would not preserve the Status
  type).

* internal/test/enum_via_oneof/enum_via_oneof_test.go: `var m
  MixedOneOf = s` (where `s` is a plain string) IS the test. The
  redundancy is intentional -- if MixedOneOf were a `type MixedOneOf
  string` newtype rather than the alias `type MixedOneOf = string`,
  this assignment would fail to compile. Letting inference give `m`
  the type `string` would lose the property under test. Added
  `//nolint:staticcheck` with the rationale inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that kin doesn't require Go 1.26, downgrade to Go 1.25 and
rebase changes from main.
Add OpenApi 3.1 top level schema sources to the hoisting test.
@mromaszewicz
mromaszewicz force-pushed the feat/kin-openapi-3.1 branch from 38df03d to 329414b Compare May 4, 2026 03:09
@jamietanna

Copy link
Copy Markdown
Member

(note to me - we should double check everything in https://learn.openapis.org/upgrading/v3.0-to-v3.1.html is covered)

@zelch

zelch commented May 8, 2026

Copy link
Copy Markdown

I'm seeing the same unhandled Schema type: &[null] on 329414b, is this expected?

Edit: {"type": ["string", "null"], "title": "nullable type"} works as expected, while {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "nullable type"} does not.

@mromaszewicz

Copy link
Copy Markdown
Member Author

No, it's not expected. I'll look at it in a little while. Very busy with other things right now. Thanks for letting us know.

Types.Is() is too strict for out needs. We want Is("object") to return
true for [object, "null"], but it's an exact match, replace this with
our own alternative that behaves as we want.
Resolved conflicts:
- pkg/codegen/operations.go: kept new IsWebhook/WebhookName/IsCallback/CallbackName
  fields from feat branch; adopted upstream's tighter column alignment for the
  pre-existing IsAlias/AliasTarget/PathItemRef fields.
- internal/test/anonymous_inner_hoisting/client.gen.go: kept ours; will be
  regenerated via make generate.
Resolved one conflict in pkg/codegen/codegen.go: upstream now uses the
collectComponentTypes / collectOperationTypes / renderBoilerplate helper
decomposition (from PR #2366). Kept that structure and changed the op-
related helper calls to use allOps (paths + webhooks + callbacks)
rather than ops, matching what the previous GenerateTypeDefinitions(t,
spec, allOps, ...) call did, so webhook/callback-derived types continue
to be emitted.
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

…nymous-schemas

The new `output-options.generate-types-for-anonymous-schemas` flag is
supposed to name every inline schema that would otherwise become an
anonymous Go struct. For top-level array schemas whose items are an
inline object, this guarantee was not being met -- the generated type
was still `type Foo = []struct { ... }`.

The auto-hoist block inside `GenerateGoSchema` (schema.go:679) is
gated on `len(path) > 1` so top-level component schemas don't trigger
it (they already have their own names). The array-items recursive call
at schema.go:1027 passes `path` unchanged to `GenerateGoSchema`, so for
a top-level array schema the path length stays at 1 and the auto-hoist
declines to fire on the inline-object items. The existing array-item
hoist further down only fires for items with `additionalProperties` or
`oneOf`/`anyOf`, leaving plain inline objects to fall through.

Fix: extend the existing array-item hoist condition (schema.go:1032)
with a third clause matching the auto-hoist's predicate -- if the flag
is on and the items schema has `properties`, hoist it to a named
element type. The naming uses the same path + "Item" convention the
hoist has always used for unions and additionalProperties items, so a
top-level `Roles` array of inline-object items emits as
`type Roles_Item struct {...}` and `type Roles = []Roles_Item`.

For arrays nested inside other schemas (path length >= 2), the
auto-hoist already fires inside the items recursive call and sets
`RefType`, so the new clause's `RefType == ""` guard correctly skips
the second hoist -- no double-emission or naming conflict.

Regression coverage: the existing global anonymous_inner_hoisting
fixture gains a `Roles` component (array of inline objects) and a
`GET /roles` operation that returns it. The operation reference keeps
the default prune pass from stripping the component before codegen
sees it. A new test asserts that the element type is named
`Roles_Item` by constructing slice literals of the named type --
without the fix, the test does not compile because the generator
inlines the element struct.

Closes Greptile P1 review comment on pkg/codegen/schema.go:1027-1032.
…ity markers

OpenAPI 3.1 supports two equivalent idioms for expressing nullability
on a schema:

  1. `type: ["string", "null"]`                  (the type-array idiom)
  2. `anyOf: [{type: string}, {type: "null"}]`   (the union idiom)

The first form has worked since the kin-openapi-3.1 branch was opened.
The second form crashed the code generator with:

    error generating type for ...: unhandled Schema type: &[null]

`generateUnion` (schema.go) walks each `anyOf`/`oneOf` element and
calls `GenerateGoSchema` on it. For a bare `{"type": "null"}` branch,
the schema's Type slice is exactly `["null"]`. `schemaPrimaryType`
only strips "null" when the slice has more than one element, so the
single-element `["null"]` survives. Inside the primitive-type dispatch
in `oapiSchemaToGoType`, none of the type branches (string, integer,
number, boolean, array, object) handles "null" alone, so the function
falls through to the `unhandled Schema type` error.

Fix has three parts:

1. `isNullTypeSchema` helper: predicate for a bare `{"type": "null"}`
   schema (type slice is exactly `["null"]`).

2. `schemaIsNullable` extension: in addition to checking whether the
   outer type array includes "null", inspect anyOf/oneOf for null-only
   branches. This lets the nullability flow through to call sites that
   wrap the result in a pointer (or `nullable.Nullable[T]`) regardless
   of which idiom the spec author used.

3. `generateUnion` collapse: after filtering null-only branches, if
   exactly one effective branch remains and there's no discriminator,
   treat the schema as that single branch rather than wrapping it in
   a one-variant union type. Together with the schemaIsNullable
   extension, this makes the two idioms produce identical Go shapes
   -- `anyOf: [{type: string}, {type: "null"}]` and `type: ["string",
   "null"]` both emit a `*string` field, not a `Pet_NicknameAnyOf`
   wrapper struct with a `FromX`/`AsX` accessor API for a single
   variant. The collapse is gated on the original anyOf/oneOf having
   contained a null branch so behavior is unchanged for pre-existing
   single-branch anyOf specs that may rely on the wrapper shape.

A small companion guard in `GenerateGoSchema` skips the
`GenStructFromSchema` overwrite when the collapse cleared the
struct-shaped fields (Properties, AdditionalProperties, UnionElements
all empty); without it the primitive GoType the collapse set would be
clobbered by an empty `struct {}` literal.

Regression coverage: two new properties on the OpenAPI 3.1 Pet schema
in internal/test/openapi31_nullable/ -- one using `anyOf: [{type:
string}, {type: "null"}]` and one using the matching `oneOf` form.
The test asserts the fields are `*string` (compile-time check via
`&nick` assignment where `nick` is a `string`) and that JSON
round-trip semantics match those of the existing type-array `nickname`
field.
…es, not raw element count

The null-branch skip introduced in 2d6387e (`codegen: treat
{"type": "null"} branches in anyOf/oneOf as nullability markers`)
filters bare null-type branches out of the union loop, but the
completeness check at the end still compared against `len(elements)`
-- the raw input count, which includes the skipped null branches.

A valid OpenAPI 3.1 nullable discriminated union like

    oneOf:
      - $ref: '#/components/schemas/Cat'
      - $ref: '#/components/schemas/Dog'
      - type: "null"
    discriminator:
      propertyName: kind

would map the two real branches (Cat, Dog) but then fail generation
with `discriminator: not all schemas were mapped` because the check
saw `len(Mapping) == 2 < len(elements) == 3`. The null branch is a
nullability marker, not a real variant that needs a discriminator
mapping.

Fix: compare against the `effectiveCount` already tracked for the
collapse path -- the number of non-null branches actually processed
by the loop. With this, nullable discriminated unions complete
generation; non-nullable unions are unchanged (effectiveCount equals
len(elements) when there are no null branches).

Regression coverage: a `DiscriminatedPet` schema is added to the
existing openapi31_nullable fixture as a Cat/Dog/null discriminated
union, exposed via a new `favorite` property on Pet so it's not
pruned. A new test asserts that JSON payloads for both Cat and Dog
round-trip through the generated discriminated-union accessors;
without the fix, codegen fails before the test can run.

Closes Greptile P1 review comment on pkg/codegen/schema.go:1449.
…ords

OpenAPI 3.1 replaced the 3.0 `format: binary` and `format: byte`
idioms for declaring binary content with the JSON-Schema-aligned
`contentMediaType` and `contentEncoding` keywords. The official 3.0->
3.1 upgrade guide
(https://learn.openapis.org/upgrading/v3.0-to-v3.1.html, "Update
file upload descriptions") instructs users to migrate to the new
keywords; without codegen support for them, a spec that follows the
guide loses its file/binary typing -- fields that used to generate as
`openapi_types.File` or `[]byte` silently fall through to bare
`string`.

Fix: in `oapiSchemaToGoType`'s string branch, synthesize a `format`
value from `contentMediaType`/`contentEncoding` when the schema does
not declare an explicit `format`. The mapping mirrors the 3.0
equivalents:

  - contentMediaType set (any value)  -> format "binary" -> openapi_types.File
  - contentEncoding set (any value)   -> format "byte"   -> []byte
  - both set                          -> contentMediaType wins (file)

An explicit `format` on the schema always wins over the synthesis, so
a `format: date` field with a stray `contentMediaType` keeps its
existing `openapi_types.Date` Go type and user `type-mapping`
overlays continue to apply unchanged. Synthesis is gated on
`globalState.is31` so 3.0 specs are untouched even if the parser
happens to populate the fields.

Regression coverage: new `internal/test/openapi31_content_keywords/`
test exercises five cases via a single `FileUploadFields` component
schema: contentMediaType -> File, contentMediaType: image/png -> File
(non-octet-stream still routes to File), contentEncoding: base64 ->
[]byte, both keywords -> File (contentMediaType wins), explicit
`format: date` overrides the synthesis. Assignments to typed zero
values fail to compile if any case regresses, so the regression
surfaces at `go build` time before any assertion runs.

First of several follow-ups identified by the v3.0->v3.1 upgrade-
guide audit; the remaining gap (empty-schema request bodies under
binary content types like `application/octet-stream: {}`) is more
invasive and will follow as a separate commit.
Comment thread pkg/codegen/schema.go Outdated
Go's encoding/json codec for `[]byte` uses standard padded base64
(RFC4648 section 4) unconditionally. The previous fix
(3ad1efd9, "handle 3.1 contentEncoding/contentMediaType file-upload
keywords") mapped *any* non-empty `contentEncoding` value to `[]byte`,
which silently corrupts every RFC4648 variant except standard base64:

  - `contentEncoding: base64url`: URL-safe characters `-` and `_`
    fail to decode, and re-marshal emits standard base64 instead of
    the declared URL-safe form. The generated client/server stops
    honoring the wire encoding declared in the spec.

  - `contentEncoding: base32` / `base16`: the JSON []byte codec
    expects/emits base64; spec-declared base32/base16 payloads round-
    trip as garbage.

  - `contentEncoding: quoted-printable`: same shape -- the JSON
    []byte codec doesn't speak QP at all.

Fix: narrow the synthesis to `contentEncoding == "base64"` only.
Other encodings fall through to the default `string` mapping,
preserving the raw declared wire form in user hands. Users who want
typed handling for the non-standard encodings can layer their own
codec at the application boundary or supply a `type-mapping`
override pointing at a wrapper type that implements an
encoding-aware MarshalJSON/UnmarshalJSON.

The `contentMediaType` mapping to `openapi_types.File` is unchanged;
that keyword expresses content semantics (raw binary of a media
type), not a wire encoding, and File is a generic blob type.

Regression coverage updates in
`internal/test/openapi31_content_keywords/`:

  - `base64UrlField` moves out of `required` and the test asserts the
    field type is `*string` (compile-time check via `&v`); a
    regression that re-introduced the []byte synthesis would break
    the build before any assertion ran.

  - `base64Field` (with standard `contentEncoding: base64`) stays
    `[]byte`, asserted by the dedicated TestContentEncodingBase64...
    test.

Closes Greptile P1 review comment on pkg/codegen/schema.go:1149+.
@mromaszewicz
mromaszewicz merged commit 2ed9ad8 into main Jun 20, 2026
28 checks passed
@mromaszewicz
mromaszewicz deleted the feat/kin-openapi-3.1 branch June 20, 2026 16:42
lwc pushed a commit to lwc/oapi-codegen that referenced this pull request Jun 23, 2026
* fix(codegen): use reflect.DeepEqual for ExclusiveMin/Max in MergeSchemas

kin-openapi's 3.1 work changed Schema.ExclusiveMin/Max from bool to
ExclusiveBound{Bool *bool; Value *float64}. Direct struct equality (!=)
still compiles but compares pointer addresses, not the wrapped values, so
two schemas with semantically identical bounds would incorrectly fail the
merge. Use reflect.DeepEqual for value-aware comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): OpenAPI 3.1 foundation -- version awareness and nullable

This is Phase 1 of bringing 3.1 feature parity from oapi-codegen-exp into
mainline. It establishes the architectural patterns the rest of the work
will follow.

Version awareness:
* New globalState.is31 flag, populated once at Generate() entry from
  swagger.IsOpenAPI31OrLater(). All version-aware logic lives in Go and
  branches on this flag; templates remain version-blind and consume
  pre-computed derived fields.
* Comment on the field explicitly forbids exposing it to TemplateFunctions
  to prevent layering violations.

Nullable handling:
* New schemaIsNullable() helper that branches on globalState.is31:
  3.0 reads s.Nullable; 3.1 reads s.Type.Includes("null").
* New schemaPrimaryType() helper strips "null" from the type slice in
  3.1 mode so existing dispatch (`*Types.Is("string")` etc.) keeps
  working for type:["string","null"].
* Four direct .Nullable read sites switched to the helper:
  schema.go (property construction, array nullable, additionalProperties)
  and operations.go (response header).
* merge_schemas.go's Nullable comparison routed through the helper; an
  explanatory comment notes that type merging itself is NOT version
  branched (equalTypes() handles single-element 3.0 and multi-element
  3.1 type slices identically), and that result.Type already carries
  any "null" entry forward in 3.1 mode.

Other:
* Adds OutputOptions.SkipEnumViaOneOf for Phase 2 (enum-via-oneOf
  detection); declared now to keep the config schema stable across
  phases.
* Removes the stale "OpenAPI 3.1.x is not yet supported" stderr
  warning at cmd/oapi-codegen entry; partial 3.1 support now exists
  and the message will become misleading as later phases land.
* Two path parameters in pkg/codegen/test_specs/x-go-type-import-pet.yaml
  were missing the OpenAPI-required `required: true`. The omission was
  incidental to what the test exercises (x-go-type-import); the spec is
  now spec-conformant.

Tests:
* internal/test/openapi31_nullable/ -- two specs (3.0 nullable:true,
  3.1 type:["string","null"]) generate into spec_3_0/ and spec_3_1/
  subpackages. Instantiation tests (no string-matching) assert both
  produce *string and round-trip JSON identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): OpenAPI 3.1 enum-via-oneOf detection

Phase 2 of the 3.1 backport. Detects the 3.1 enum-via-oneOf idiom and
routes it through the existing typed-enum codegen path so users see the
same Go shape they'd get from a plain `enum:` array.

The idiom (per OpenAPI 3.1 / JSON Schema 2020-12):

    Severity:
      type: integer        # or "string"
      oneOf:
        - title: HIGH
          const: 2
          description: An urgent problem
        - title: MEDIUM
          const: 1
        - title: LOW
          const: 0

Generates:

    type Severity int
    const (
        HIGH   Severity = 2
        LOW    Severity = 0
        MEDIUM Severity = 1
    )

Implementation:
* New detectEnumViaOneOf() helper in pkg/codegen/schema.go. Trigger
  conditions: globalState.is31 is true, !SkipEnumViaOneOf, schema's
  primary type is "string" or "integer", every oneOf branch carries
  non-empty Title AND non-nil Const, and no branch is itself a
  composition or has Properties.
* GenerateGoSchema short-circuits between the AllOf branch and the
  type dispatch: when the idiom matches, populate Schema.EnumValues
  directly and register an AdditionalType for non-toplevel schemas
  so the existing EnumDefinition collector picks them up. Negative
  matches fall through to the standard oneOf union generator.
* No template changes -- the existing constants.tmpl renders the
  typed enum + Valid() method via the standard EnumDefinition path.
* The OutputOptions.SkipEnumViaOneOf flag declared in Phase 1 is
  now actually consulted here.

Tests:
* internal/test/enum_via_oneof/ -- ports the exp test fixture
  (Severity int, Color string, MixedOneOf negative path). Five
  instantiation tests (no string-matching) cover constant values,
  JSON round-trips, and the negative path's compile-time alias
  shape (a plain string is directly assignable to MixedOneOf,
  proving it did NOT become a typed-enum newtype).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): OpenAPI 3.1 webhooks -- WebhookInitiator + WebhookReceiver

Phase 3 of the 3.1 backport. Generates client-side and server-side
support for `webhooks:` declared in OpenAPI 3.1 specs. The output
mirrors the existing Client / Server interfaces in shape but lives in
its own types, matching the conventions oapi-codegen-exp established.

Data model:
* OperationDefinition gains IsWebhook bool and WebhookName string.
* New WebhookOperationDefinitions() walks swagger.Webhooks and produces
  OperationDefinitions in the same shape as path operations, minus the
  path-alias and path-parameter logic (webhooks have no path template).
  No version gate -- kin-openapi only populates the Webhooks field for
  3.1+ documents, so 3.0 specs short-circuit on the empty map.

Generated client (WebhookInitiator):
* New pkg/codegen/templates/webhook-initiator.tmpl, modeled on
  client.tmpl. Emits:
    type WebhookInitiator struct{ Client; RequestEditors }
    NewWebhookInitiator + WebhookInitiatorOption + WithWebhookHTTPClient
        + WithWebhookRequestEditorFn
    WebhookInitiatorInterface
    per-webhook methods (Op + OpWithBody) and request builders
        (NewOpWebhookRequest + NewOpWebhookRequestWithBody)
* No stored Server -- the target URL is supplied per-call by the
  caller (typically discovered from a subscription registration).
* Gated by Generate.Client (paired with the path Client).

Generated server (WebhookReceiver, stdhttp):
* New pkg/codegen/templates/stdhttp/std-http-webhook-receiver.tmpl.
  Emits:
    type WebhookReceiverInterface { HandleOpWebhook(w, r) for each }
    type WebhookReceiverMiddlewareFunc func(http.Handler) http.Handler
    OpWebhookHandler(si, middlewares...) http.Handler -- per-webhook
        factory; caller mounts the returned http.Handler at the URL
        path they advertise to subscribers.
* Methods take plain (w, r) -- the user reads/parses the body
  themselves. Simpler than the full ServerInterfaceWrapper machinery;
  parameter binding can be added later if the demand surfaces.
* Gated by Generate.StdHTTPServer (paired with the path stdhttp
  ServerInterface). Other frameworks are left as follow-ups.

Wiring (codegen.go):
* allOps = ops + webhookOps is passed to OperationImports and
  GenerateTypeDefinitions so webhook bodies and responses generate
  their type definitions and imports normally.
* New GenerateWebhookInitiator and GenerateStdHTTPWebhookReceiver entry
  points in operations.go, modeled on GenerateClient and
  GenerateStdHTTPServer.
* Output ordering: webhook initiator follows ClientWithResponses;
  webhook receiver follows the path StdHTTPServer block.

Tests (internal/test/webhooks/):
* spec.yaml declares a single petStatusChanged webhook with a JSON
  PetStatusEvent body and a 204 response.
* TestWebhookRoundTrip mounts the generated factory against an
  httptest.Server and fires a webhook via the initiator; asserts the
  payload, method, and Content-Type round-trip intact.
* TestWebhookInitiatorRequestEditor verifies WithWebhookRequestEditorFn
  applies on every outgoing request (parity with the path Client).
* TestWebhookReceiverMiddleware documents middleware composition order:
  the factory wraps in `for _, mw := range middlewares { h = mw(h) }`,
  so the LAST middleware passed becomes the outermost wrapper.

Example (examples/webhook/):
* Door badge reader scenario ported from oapi-codegen-exp/examples/webhook.
  The server randomly generates enter/exit badge events every second
  and fires the appropriate webhook to all registered subscribers; the
  client subscribes to both event kinds via the path API, runs a local
  HTTP receiver to handle inbound events, and deregisters cleanly on
  exit. Demonstrates the full Client/Server + WebhookInitiator/Receiver
  pairing in one example.
* Lives as a regular package under examples/go.mod -- no separate
  module, no tools.go shim. `make generate` from examples/ regenerates
  via the package's go:generate directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): OpenAPI callbacks -- CallbackInitiator + CallbackReceiver

Phase 4 of the 3.1 backport. Generates client-side and server-side
support for `callbacks:` blocks nested under path operations. The output
mirrors WebhookInitiator / WebhookReceiver in shape -- separate types
named with the `Callback` prefix to match oapi-codegen-exp.

Callbacks have been part of the OpenAPI spec since 3.0, so this is NOT
version-gated: any spec that declares callbacks gets them generated.

Data model:
* OperationDefinition gains IsCallback bool and CallbackName string
  (alongside the IsWebhook / WebhookName pair from Phase 3).
* New CallbackOperationDefinitions() walks
  spec.Paths.<path>.<method>.Callbacks. Each leaf operation under the
  callback's URL-expression key becomes one OperationDefinition with
  IsCallback=true and CallbackName set to the outer map key (e.g.
  "treePlanted"). The codegen does not interpret the URL expression
  itself -- the caller of CallbackInitiator supplies the resolved
  target URL at runtime, the same way it does for a webhook.
* Callback's internal map is private; iterate via cb.Keys() / cb.Value()
  with sorted keys for deterministic output.

Generated client (CallbackInitiator):
* New pkg/codegen/templates/callback-initiator.tmpl. Same shape as the
  Phase 3 webhook-initiator.tmpl with `Webhook` -> `Callback` rename:
    type CallbackInitiator struct{ Client; RequestEditors }
    NewCallbackInitiator + CallbackInitiatorOption
        + WithCallbackHTTPClient + WithCallbackRequestEditorFn
    CallbackInitiatorInterface
    per-callback methods (Op + OpWithBody) and request builders
        (NewOpCallbackRequest + NewOpCallbackRequestWithBody)
* No stored Server -- targetURL is per-call.
* Gated only by Generate.Client + non-empty callbackOps.

Generated server (CallbackReceiver, stdhttp):
* New pkg/codegen/templates/stdhttp/std-http-callback-receiver.tmpl.
* WebhookReceiverInterface analog: Handle{Op}Callback(w, r) plus a
  per-callback factory function {Op}CallbackHandler(si, middlewares...)
  http.Handler.
* Gated only by Generate.StdHTTPServer + non-empty callbackOps.

Wiring (codegen.go):
* allOps is now `ops + webhookOps + callbackOps` so type-definition
  and import passes see callback bodies / responses.
* Callback initiator output follows webhook initiator (so the order is
  Client / ClientWithResponses / WebhookInitiator / CallbackInitiator).
* Callback receiver output follows webhook receiver under the
  StdHTTPServer block.
* New GenerateCallbackInitiator and GenerateStdHTTPCallbackReceiver
  template entry points in operations.go.

Tests (internal/test/callbacks/):
* spec.yaml deliberately uses `openapi: 3.0.3` to verify the codegen
  does NOT gate callback emission on 3.1 (callbacks are 3.0+).
  Models a tree-planting flow: parent operation PlantTree, callback
  treePlanted with TreePlantingResult body.
* TestCallbackRoundTrip mounts the generated factory against an
  httptest.Server and fires a callback via the initiator; asserts the
  payload, method, and Content-Type round-trip intact.
* TestCallbackInitiatorRequestEditor verifies WithCallbackRequestEditorFn
  applies on every outgoing request (parity with the path Client and
  the WebhookInitiator).

Example (examples/callback/):
* Tree farm scenario ported from oapi-codegen-exp/examples/callback.
  Server accepts PlantTree, returns 202, sleeps 1-5s, then fires the
  treePlanted callback to the caller-supplied URL. Client opens an
  ephemeral receiver, fires 10 plantings in sequence, and waits on a
  channel until all 10 callbacks have arrived.
* Lives as a regular package under examples/go.mod -- no separate
  module, no tools.go shim. Same flat layout as examples/webhook/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): OpenAPI 3.1 polish -- examples in doc comments + const

Phase 5 of the 3.1 backport. Two small additive features that round
out the 3.1 surface; no template changes.

`examples:` (plural array) → Go doc comments:
* New describeWithExamples() helper in pkg/codegen/schema.go folds
  example data into the description string used for generated doc
  comments. Version-aware: 3.0 reads schema.Example (singular), 3.1
  reads schema.Examples (plural array). Cross-version misuse is
  documented as invalid input and the helper does not look at the
  off-version field.
* Non-string examples are JSON-encoded so structured values render
  readably; strings round-trip as themselves.
* Applied at the three sites where Description flows from the spec
  schema into the generated Schema/Property: GenerateGoSchema's $ref
  short-circuit, the main outSchema construction, and per-property
  population during object walk.

`const:` → typed alias + singleton constant:
* The existing enum branch in GenerateGoSchema now also fires on
  `(globalState.is31 && schema.Const != nil)`. A local enumSource
  abstracts over the two cases so a scalar `const: X` schema emits the
  same shape as `enum: [X]`: a typed alias plus a sanitized constant
  derived from the value.
* Falls through the standard EnumDefinition pipeline -- existing
  constants.tmpl renders the typed enum + Valid() method without
  changes.

Paths optional in 3.1: no-op. Every swagger.Paths access site
(filter.go, prune.go, gather.go, operations.go, the new
WebhookOperationDefinitions and CallbackOperationDefinitions) already
nil-guards.

Tests (internal/test/openapi31_polish/):
* Pet.Name has a description plus two string examples; Pet.Lives has
  one integer example. Status is a top-level `const: active` schema.
* TestStatusConstSchema (instantiation): `var s Status = Active`
  compiles only because Status is a distinct named type, and asserts
  the constant's value plus that Valid() returns true.
* TestPetExampleComments parses the generated Go source and walks the
  AST to extract Pet's per-field doc comments, then asserts the
  expected description and "Examples: ..." fragments. Doc comments
  aren't runtime-introspectable, so AST inspection of the generated
  file is the right granularity here -- not a brittle string-match
  against the file as a whole.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(codegen): unify stdhttp webhook+callback receiver template, add param binding

Phase 6.1 of the 3.1 backport (kicking off the per-framework receiver
work). Sets up the prototype shape that the other 7 server-framework
receivers will follow.

Two changes that always have to land together:

1. Unify the two receiver templates into one parameterized template,
   matching oapi-codegen-exp's structure:
   * Replaces pkg/codegen/templates/stdhttp/std-http-webhook-receiver.tmpl
     and pkg/codegen/templates/stdhttp/std-http-callback-receiver.tmpl
     (deleted) with a single std-http-receiver.tmpl. The new template
     takes a Prefix data field ("Webhook" or "Callback") and emits the
     correct interface, middleware type, and per-op handler factory.
   * New ReceiverTemplateData struct + NewReceiverTemplateData
     constructor in operations.go feed the template.
   * Single GenerateStdHTTPReceiver(t, prefix, ops) entry point;
     callers in Generate() pass "Webhook" or "Callback".

2. Add query/header parameter binding to the receiver factory,
   matching mainline's existing path-server middleware pattern.
   The factory now takes an errHandler argument:

       func PetStatusChangedWebhookHandler(
           si WebhookReceiverInterface,
           errHandler func(w http.ResponseWriter, r *http.Request, err error),
           middlewares ...WebhookReceiverMiddlewareFunc,
       ) http.Handler

   When operations have query/header params (rare for webhooks but
   common for callbacks), the inline closure binds them into a typed
   {Op}Params struct via runtime.BindQueryParameterWithOptions /
   runtime.BindStyledParameterWithOptions and routes any error through
   errHandler. errHandler may be nil; the default returns 400 with the
   error message. When operations have no params, errHandler is
   unused -- pass nil and the prior shape's behavior is preserved.

   This is a breaking change to anyone using the Phase 3/4 generated
   *Handler(si, middlewares...) shape. Acceptable since the Phase 3/4
   surface is unreleased and only-this-branch.

New helper: OperationDefinition.SourceName() returns WebhookName when
IsWebhook, CallbackName when IsCallback. Templates use it to label
emitted handlers uniformly without branching on the source kind.

Existing call sites updated to pass nil for errHandler:
  internal/test/webhooks/webhooks_test.go      (3 places)
  internal/test/callbacks/callbacks_test.go    (2 places)
  examples/webhook/client/main.go              (2 places)
  examples/callback/client/main.go             (1 place)

The four affected gen files are regenerated. Other gen files in the
tree have unrelated drift from Phase 5's examples-in-doc-comments
feature (those don't touch the new receiver shape); a follow-up
`make generate` commit picks them up cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(gen): regenerate gen files to pick up Phase 5 examples-in-doc-comments

Phase 5 (eedf057) added the describeWithExamples() helper that folds
schema.Example / schema.Examples into the description string used for
generated Go doc comments. Existing test specs that already had
`example:` set on their schemas now produce richer doc comments.

This commit is purely the result of running `make generate` -- no
behavior change, only doc-comment additions like:

    // Code The underlying http status code
    +//
    +// Example: 500
    Code int32 `json:"code"`

Touched files:
  examples/minimal-server/stdhttp/api/ping.gen.go
  internal/test/externalref/petstore/externalref.gen.go
  internal/test/issues/issue-1087/deps/deps.gen.go
  internal/test/issues/issue-1168/api.gen.go
  internal/test/issues/issue1561/issue1561.gen.go

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): chi webhook+callback receiver template

Phase 6.2 of the per-framework receiver work. Chi's path-handler
signature is identical to stdhttp's (`(w http.ResponseWriter, r
*http.Request)`), so the receiver template is structurally identical;
only the file path and Go entry point are new.

* New pkg/codegen/templates/chi/chi-receiver.tmpl -- a copy of the
  Phase 6.1 unified stdhttp receiver template. Single template handles
  both webhook and callback receivers via the Prefix data field.
* New GenerateChiReceiver() entry point in operations.go.
* In Generate(): two new blocks gated on Generate.ChiServer + non-empty
  webhook/callback ops, mirroring the stdhttp blocks. Generated output
  is written inside the existing chi-server WriteString block so all
  chi-related output stays grouped.
* New internal/test/webhooks_chi/ -- compile-time assertion that the
  chi receiver template produces valid Go. The runtime round-trip
  behavior is already covered by internal/test/webhooks (stdhttp);
  since chi shares the signature, that coverage transfers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): gorilla/mux webhook+callback receiver template

Phase 6.3. Gorilla shares stdhttp's `(w, r)` handler signature, so the
receiver template is structurally identical -- same approach as the
Phase 6.2 chi addition.

* New pkg/codegen/templates/gorilla/gorilla-receiver.tmpl
* New GenerateGorillaReceiver() entry point
* Wiring in Generate() gated on Generate.GorillaServer; output written
  inside the existing gorilla-server WriteString block
* New internal/test/webhooks_gorilla/ as a compile-time assertion;
  runtime round-trip behavior is covered by internal/test/webhooks
  (signature is identical, so coverage transfers)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): echo (v4) webhook+callback receiver template

Phase 6.4. First framework with a non-stdhttp signature, requiring its
own bespoke template.

Echo's idioms:
* Method signature `Handle{Op}{Prefix}(ctx echo.Context, params...) error`
  (echo v4 uses the non-pointer Context).
* Factory returns `echo.HandlerFunc`; middlewares are echo's native
  `echo.MiddlewareFunc` (`func(echo.HandlerFunc) echo.HandlerFunc`).
* Parameter-binding errors are returned via echo.NewHTTPError so
  echo's framework error chain reports them as 400. There's no
  errHandler argument like the stdhttp factory has.
* Query params bind via ctx.QueryParam / ctx.QueryParams() (and
  runtime.BindQueryParameterWithOptions for styled). Header params
  read ctx.Request().Header.

* New pkg/codegen/templates/echo/echo-receiver.tmpl
* New GenerateEchoReceiver() entry point
* Wiring in Generate() gated on Generate.EchoServer; output written
  inside the existing echo-server WriteString block
* New internal/test/webhooks_echo/ -- compile-time assertion that the
  echo receiver template produces valid Go

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): echo (v5) webhook+callback receiver template

Phase 6.5. Echo v5 differs from v4 only in using `*echo.Context`
(pointer) -- otherwise the API surface (ctx.QueryParam, ctx.Request(),
echo.NewHTTPError, echo.HandlerFunc, echo.MiddlewareFunc) is identical.
The template is a `echo.Context` -> `*echo.Context` substitution from
the Phase 6.4 v4 template.

* New pkg/codegen/templates/echo/v5/echo-receiver.tmpl
* New GenerateEcho5Receiver() entry point
* Wiring in Generate() gated on Generate.Echo5Server; output written
  inside the existing echo5-server WriteString block

No internal/test/webhooks_echo5/ directory: echo v5 is not in
internal/test/go.mod (the existing echov5 fixture lives in its own
sub-module under internal/test/parameters/echov5/), so adding a
webhook test would require either invasive dep changes or another
sub-module. The Phase 6.4 echo v4 test exercises the same template
shape end-to-end -- inspection of the v5 codegen output confirms only
the Context pointer-ness changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): gin webhook+callback receiver template

Phase 6.6. Gin's idioms:
* Method signature `Handle{Op}{Prefix}(c *gin.Context, params...)`
  with no error return -- gin reports errors via the context.
* Factory returns `gin.HandlerFunc`. Per-handler middleware is NOT
  generated (gin's idiom prefers route-group / engine .Use()
  composition); users mount middleware at the engine level.
* Parameter-binding errors abort the request with
  c.JSON(http.StatusBadRequest, gin.H{"error": "..."}).
* Query params bind via c.Query / c.Request.URL.Query() (and
  runtime.BindQueryParameterWithOptions for styled). Header params
  read c.Request.Header.

* New pkg/codegen/templates/gin/gin-receiver.tmpl
* New GenerateGinReceiver() entry point
* Wiring in Generate() gated on Generate.GinServer
* New internal/test/webhooks_gin/ -- compile-time assertion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): fiber (v2) webhook+callback receiver template

Phase 6.7. Fiber's idioms:
* Method signature `Handle{Op}{Prefix}(c *fiber.Ctx, params...) error`
  (mainline pins fiber v2 -> *fiber.Ctx pointer; v3 changes to a
  non-pointer interface but is not pinned in mainline).
* Factory returns `fiber.Handler` (alias for `func(*fiber.Ctx) error`).
* Parameter-binding errors are returned via `fiber.NewError(
  fiber.StatusBadRequest, ...)` so fiber's error chain reports them
  as 400.
* Query-param binding via `url.ParseQuery(string(c.Request().URI().
  QueryString()))` to get url.Values for runtime.BindQueryParameter
  WithOptions, plus `c.Query("name")` for direct passthrough/JSON.
* Header params read `c.GetReqHeaders()` which returns
  map[string][]string.
* Per-handler middleware is NOT generated; users compose middleware
  via fiber.App.Use().

* New pkg/codegen/templates/fiber/fiber-receiver.tmpl
* New GenerateFiberReceiver() entry point
* Wiring in Generate() gated on Generate.FiberServer
* New internal/test/webhooks_fiber/ -- compile-time assertion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(codegen): iris webhook+callback receiver template

Phase 6.8, completing the per-framework receiver fan-out. Iris's
idioms:
* Method signature `Handle{Op}{Prefix}(ctx iris.Context, params...)`
  with no error return -- iris reports errors via the context.
* Factory returns `iris.Handler`. Per-handler middleware is NOT
  generated; iris's idiom prefers app.Use() / Party-level composition.
* Parameter-binding errors set ctx.StatusCode(400) plus
  ctx.WriteString(reason) and return.
* Query params bind via ctx.URLParam("name") for direct values, and
  runtime.BindQueryParameterWithOptions with ctx.Request().URL.Query()
  for styled. Header params read ctx.Request().Header.

* New pkg/codegen/templates/iris/iris-receiver.tmpl
* New GenerateIrisReceiver() entry point
* Wiring in Generate() gated on Generate.IrisServer
* New internal/test/webhooks_iris/ -- compile-time assertion

All seven server frameworks (stdhttp/chi/gorilla/echo/echo5/gin/
fiber/iris) now emit framework-native webhook+callback receiver
interfaces alongside their path-server interfaces, gated by the
existing per-framework Generate.* flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(test): consolidate webhook tests into internal/test/webhooks/<framework>/

Final Phase 6 cleanup. Reorganizes the seven flat per-framework
webhook test directories under a single internal/test/webhooks/
parent so they group together and the layout reflects the
framework-fan-out done in Phases 6.1-6.8.

Layout change:
  internal/test/webhooks/                 (the original stdhttp tests)
  internal/test/webhooks_chi/             }
  internal/test/webhooks_echo/            }
  internal/test/webhooks_fiber/           }  flat per-framework dirs
  internal/test/webhooks_gin/             }
  internal/test/webhooks_gorilla/         }
  internal/test/webhooks_iris/            }

becomes:

  internal/test/webhooks/
  ├── spec.yaml          (shared by all subdirs)
  ├── stdhttp/           (original round-trip tests)
  ├── chi/
  ├── echo/
  ├── fiber/
  ├── gin/
  ├── gorilla/
  └── iris/

Per-subdir changes:
* Package name simplified from `webhooks` / `webhooks_<framework>` to
  just `<framework>` (matches dir name; package-vs-import name overlap
  with framework packages like chi/v5 is harmless since Go's local
  package name is implicit).
* Generated file uniformly named `webhooks.gen.go` (was
  `webhooks_<framework>.gen.go` in flat dirs).
* config.yaml `package:` and `output:` directives updated; schema
  reference path bumped one level deeper.
* doc.go `package` and prose updated; `go:generate` now points at the
  shared `../spec.yaml`.

Spec deduplication:
* The seven copies of spec.yaml (functionally identical, only
  title/description varied) collapse to one shared file at
  internal/test/webhooks/spec.yaml. Each subdir's go:generate
  references it via `../spec.yaml`.

The round-trip test in stdhttp/webhooks_test.go has its package
declaration updated to `package stdhttp`.

No code or template changes outside the moves; same gen output up to
package name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(codegen): address greptile review findings on Phase 6 work

Two findings from automated review:

P1 -- Non-deterministic map iteration in CallbackOperationDefinitions
(pkg/codegen/operations.go).

The inner loop walked `pathItem.Operations()` directly. When a path
declares callbacks on multiple HTTP methods (e.g. both POST and PUT),
the generated output would differ between runs because Go map
iteration is randomized. Fix: copy keys into a slice and walk via
SortedMapKeys, matching the pattern used by every other similar loop
in the codebase (including WebhookOperationDefinitions immediately
above).

P2 -- Hidden global-state dependency in version-aware helpers
(pkg/codegen/schema.go).

schemaIsNullable, schemaPrimaryType, describeWithExamples, and
detectEnumViaOneOf all read globalState.is31 implicitly, with no
indication at the function signature that callers must invoke them
only after Generate() has initialized the global. Fix: add an
explicit "Precondition" line to each helper's doc comment so callers
(including future refactors that move schema resolution earlier) are
aware of the dependency. The signatures themselves are not changed --
threading the version flag through every call site would be an
intrusive refactor for a problem better described in prose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pull in kin-openapi v0.136.0

Pulls in the official kin release which adds OpenAPI 3.1 support, and
updates CI to run only on Go 1.26

* fix(test): address golangci-lint errcheck + ST1023 findings

Two distinct lint issues across four test files.

errcheck (5 sites): defer / direct calls to r.Body.Close() and
resp.Body.Close() ignored their error returns. Standard fix:
* `defer r.Body.Close()` -> `defer func() { _ = r.Body.Close() }()`
* `resp.Body.Close()` (non-deferred) -> `_ = resp.Body.Close()`

  internal/test/callbacks/callbacks_test.go         (1)
  internal/test/webhooks/stdhttp/webhooks_test.go   (4)

ST1023 (2 sites): "should omit type X from declaration; it will be
inferred from the right-hand side". Two different stories.

* internal/test/openapi31_polish/openapi31_polish_test.go: `var s
  Status = Active` is genuinely redundant -- `Active` is declared
  `const Active Status = "active"`, so type inference gives `s` the
  type `Status` already. Switched to `s := Active`. Comment updated
  to explain the still-meaningful compile-time check (if Active had
  been emitted untyped, `s := Active` would not preserve the Status
  type).

* internal/test/enum_via_oneof/enum_via_oneof_test.go: `var m
  MixedOneOf = s` (where `s` is a plain string) IS the test. The
  redundancy is intentional -- if MixedOneOf were a `type MixedOneOf
  string` newtype rather than the alias `type MixedOneOf = string`,
  this assignment would fail to compile. Letting inference give `m`
  the type `string` would lose the property under test. Added
  `//nolint:staticcheck` with the rationale inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix lint issues and CI versioning

* Rebase and downgrade Go

Now that kin doesn't require Go 1.26, downgrade to Go 1.25 and
rebase changes from main.

* Extend hoisting tests

Add OpenApi 3.1 top level schema sources to the hoisting test.

* Fix issues due to rebasing onto main

* Replace kin Types.Is() usage with our own

Types.Is() is too strict for out needs. We want Is("object") to return
true for [object, "null"], but it's an exact match, replace this with
our own alternative that behaves as we want.

* Add test coverage for nullables

* codegen: hoist inline-object array items under generate-types-for-anonymous-schemas

The new `output-options.generate-types-for-anonymous-schemas` flag is
supposed to name every inline schema that would otherwise become an
anonymous Go struct. For top-level array schemas whose items are an
inline object, this guarantee was not being met -- the generated type
was still `type Foo = []struct { ... }`.

The auto-hoist block inside `GenerateGoSchema` (schema.go:679) is
gated on `len(path) > 1` so top-level component schemas don't trigger
it (they already have their own names). The array-items recursive call
at schema.go:1027 passes `path` unchanged to `GenerateGoSchema`, so for
a top-level array schema the path length stays at 1 and the auto-hoist
declines to fire on the inline-object items. The existing array-item
hoist further down only fires for items with `additionalProperties` or
`oneOf`/`anyOf`, leaving plain inline objects to fall through.

Fix: extend the existing array-item hoist condition (schema.go:1032)
with a third clause matching the auto-hoist's predicate -- if the flag
is on and the items schema has `properties`, hoist it to a named
element type. The naming uses the same path + "Item" convention the
hoist has always used for unions and additionalProperties items, so a
top-level `Roles` array of inline-object items emits as
`type Roles_Item struct {...}` and `type Roles = []Roles_Item`.

For arrays nested inside other schemas (path length >= 2), the
auto-hoist already fires inside the items recursive call and sets
`RefType`, so the new clause's `RefType == ""` guard correctly skips
the second hoist -- no double-emission or naming conflict.

Regression coverage: the existing global anonymous_inner_hoisting
fixture gains a `Roles` component (array of inline objects) and a
`GET /roles` operation that returns it. The operation reference keeps
the default prune pass from stripping the component before codegen
sees it. A new test asserts that the element type is named
`Roles_Item` by constructing slice literals of the named type --
without the fix, the test does not compile because the generator
inlines the element struct.

Closes Greptile P1 review comment on pkg/codegen/schema.go:1027-1032.

* codegen: treat `{"type": "null"}` branches in anyOf/oneOf as nullability markers

OpenAPI 3.1 supports two equivalent idioms for expressing nullability
on a schema:

  1. `type: ["string", "null"]`                  (the type-array idiom)
  2. `anyOf: [{type: string}, {type: "null"}]`   (the union idiom)

The first form has worked since the kin-openapi-3.1 branch was opened.
The second form crashed the code generator with:

    error generating type for ...: unhandled Schema type: &[null]

`generateUnion` (schema.go) walks each `anyOf`/`oneOf` element and
calls `GenerateGoSchema` on it. For a bare `{"type": "null"}` branch,
the schema's Type slice is exactly `["null"]`. `schemaPrimaryType`
only strips "null" when the slice has more than one element, so the
single-element `["null"]` survives. Inside the primitive-type dispatch
in `oapiSchemaToGoType`, none of the type branches (string, integer,
number, boolean, array, object) handles "null" alone, so the function
falls through to the `unhandled Schema type` error.

Fix has three parts:

1. `isNullTypeSchema` helper: predicate for a bare `{"type": "null"}`
   schema (type slice is exactly `["null"]`).

2. `schemaIsNullable` extension: in addition to checking whether the
   outer type array includes "null", inspect anyOf/oneOf for null-only
   branches. This lets the nullability flow through to call sites that
   wrap the result in a pointer (or `nullable.Nullable[T]`) regardless
   of which idiom the spec author used.

3. `generateUnion` collapse: after filtering null-only branches, if
   exactly one effective branch remains and there's no discriminator,
   treat the schema as that single branch rather than wrapping it in
   a one-variant union type. Together with the schemaIsNullable
   extension, this makes the two idioms produce identical Go shapes
   -- `anyOf: [{type: string}, {type: "null"}]` and `type: ["string",
   "null"]` both emit a `*string` field, not a `Pet_NicknameAnyOf`
   wrapper struct with a `FromX`/`AsX` accessor API for a single
   variant. The collapse is gated on the original anyOf/oneOf having
   contained a null branch so behavior is unchanged for pre-existing
   single-branch anyOf specs that may rely on the wrapper shape.

A small companion guard in `GenerateGoSchema` skips the
`GenStructFromSchema` overwrite when the collapse cleared the
struct-shaped fields (Properties, AdditionalProperties, UnionElements
all empty); without it the primitive GoType the collapse set would be
clobbered by an empty `struct {}` literal.

Regression coverage: two new properties on the OpenAPI 3.1 Pet schema
in internal/test/openapi31_nullable/ -- one using `anyOf: [{type:
string}, {type: "null"}]` and one using the matching `oneOf` form.
The test asserts the fields are `*string` (compile-time check via
`&nick` assignment where `nick` is a `string`) and that JSON
round-trip semantics match those of the existing type-array `nickname`
field.

* codegen: compare discriminator mapping count against effective branches, not raw element count

The null-branch skip introduced in 2d6387e (`codegen: treat
{"type": "null"} branches in anyOf/oneOf as nullability markers`)
filters bare null-type branches out of the union loop, but the
completeness check at the end still compared against `len(elements)`
-- the raw input count, which includes the skipped null branches.

A valid OpenAPI 3.1 nullable discriminated union like

    oneOf:
      - $ref: '#/components/schemas/Cat'
      - $ref: '#/components/schemas/Dog'
      - type: "null"
    discriminator:
      propertyName: kind

would map the two real branches (Cat, Dog) but then fail generation
with `discriminator: not all schemas were mapped` because the check
saw `len(Mapping) == 2 < len(elements) == 3`. The null branch is a
nullability marker, not a real variant that needs a discriminator
mapping.

Fix: compare against the `effectiveCount` already tracked for the
collapse path -- the number of non-null branches actually processed
by the loop. With this, nullable discriminated unions complete
generation; non-nullable unions are unchanged (effectiveCount equals
len(elements) when there are no null branches).

Regression coverage: a `DiscriminatedPet` schema is added to the
existing openapi31_nullable fixture as a Cat/Dog/null discriminated
union, exposed via a new `favorite` property on Pet so it's not
pruned. A new test asserts that JSON payloads for both Cat and Dog
round-trip through the generated discriminated-union accessors;
without the fix, codegen fails before the test can run.

Closes Greptile P1 review comment on pkg/codegen/schema.go:1449.

* codegen: handle 3.1 contentEncoding/contentMediaType file-upload keywords

OpenAPI 3.1 replaced the 3.0 `format: binary` and `format: byte`
idioms for declaring binary content with the JSON-Schema-aligned
`contentMediaType` and `contentEncoding` keywords. The official 3.0->
3.1 upgrade guide
(https://learn.openapis.org/upgrading/v3.0-to-v3.1.html, "Update
file upload descriptions") instructs users to migrate to the new
keywords; without codegen support for them, a spec that follows the
guide loses its file/binary typing -- fields that used to generate as
`openapi_types.File` or `[]byte` silently fall through to bare
`string`.

Fix: in `oapiSchemaToGoType`'s string branch, synthesize a `format`
value from `contentMediaType`/`contentEncoding` when the schema does
not declare an explicit `format`. The mapping mirrors the 3.0
equivalents:

  - contentMediaType set (any value)  -> format "binary" -> openapi_types.File
  - contentEncoding set (any value)   -> format "byte"   -> []byte
  - both set                          -> contentMediaType wins (file)

An explicit `format` on the schema always wins over the synthesis, so
a `format: date` field with a stray `contentMediaType` keeps its
existing `openapi_types.Date` Go type and user `type-mapping`
overlays continue to apply unchanged. Synthesis is gated on
`globalState.is31` so 3.0 specs are untouched even if the parser
happens to populate the fields.

Regression coverage: new `internal/test/openapi31_content_keywords/`
test exercises five cases via a single `FileUploadFields` component
schema: contentMediaType -> File, contentMediaType: image/png -> File
(non-octet-stream still routes to File), contentEncoding: base64 ->
[]byte, both keywords -> File (contentMediaType wins), explicit
`format: date` overrides the synthesis. Assignments to typed zero
values fail to compile if any case regresses, so the regression
surfaces at `go build` time before any assertion runs.

First of several follow-ups identified by the v3.0->v3.1 upgrade-
guide audit; the remaining gap (empty-schema request bodies under
binary content types like `application/octet-stream: {}`) is more
invasive and will follow as a separate commit.

* codegen: only map contentEncoding to []byte for standard base64

Go's encoding/json codec for `[]byte` uses standard padded base64
(RFC4648 section 4) unconditionally. The previous fix
(3ad1efd9, "handle 3.1 contentEncoding/contentMediaType file-upload
keywords") mapped *any* non-empty `contentEncoding` value to `[]byte`,
which silently corrupts every RFC4648 variant except standard base64:

  - `contentEncoding: base64url`: URL-safe characters `-` and `_`
    fail to decode, and re-marshal emits standard base64 instead of
    the declared URL-safe form. The generated client/server stops
    honoring the wire encoding declared in the spec.

  - `contentEncoding: base32` / `base16`: the JSON []byte codec
    expects/emits base64; spec-declared base32/base16 payloads round-
    trip as garbage.

  - `contentEncoding: quoted-printable`: same shape -- the JSON
    []byte codec doesn't speak QP at all.

Fix: narrow the synthesis to `contentEncoding == "base64"` only.
Other encodings fall through to the default `string` mapping,
preserving the raw declared wire form in user hands. Users who want
typed handling for the non-standard encodings can layer their own
codec at the application boundary or supply a `type-mapping`
override pointing at a wrapper type that implements an
encoding-aware MarshalJSON/UnmarshalJSON.

The `contentMediaType` mapping to `openapi_types.File` is unchanged;
that keyword expresses content semantics (raw binary of a media
type), not a wire encoding, and File is a generic blob type.

Regression coverage updates in
`internal/test/openapi31_content_keywords/`:

  - `base64UrlField` moves out of `required` and the test asserts the
    field type is `*string` (compile-time check via `&v`); a
    regression that re-introduced the []byte synthesis would break
    the build before any assertion ran.

  - `base64Field` (with standard `contentEncoding: base64`) stays
    `[]byte`, asserted by the dedicated TestContentEncodingBase64...
    test.

Closes Greptile P1 review comment on pkg/codegen/schema.go:1149+.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.1 enhancement New feature or request notable changes Used for release notes to highlight these more highly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAPI 3.1 support?

3 participants