Skip to content

Initial OpenAPI 3.1 support based on kin-openapi#2336

Open
mromaszewicz wants to merge 25 commits into
mainfrom
feat/kin-openapi-3.1
Open

Initial OpenAPI 3.1 support based on kin-openapi#2336
mromaszewicz wants to merge 25 commits into
mainfrom
feat/kin-openapi-3.1

Conversation

@mromaszewicz
Copy link
Copy Markdown
Member

@mromaszewicz mromaszewicz commented Apr 25, 2026

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
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR adds initial OpenAPI 3.1 support to oapi-codegen, ported from an experimental branch. It introduces webhook support (3.1+), callback support (3.0+), the enum-via-oneOf idiom for typed Go enums, const schema folding, type: [..., \"null\"] nullable handling, and plural examples in doc comments. The dependency on kin-openapi is updated to v0.136.0 (stable release) and the minimum Go version is bumped to 1.26.2.

Confidence Score: 5/5

Safe to merge; only P2 observations remain — both the non-determinism P1 and the pseudo-version P1 from earlier rounds are resolved.

No P0 or P1 issues found in the changed code. The previously flagged CallbackOperationDefinitions non-determinism is fixed with SortedMapKeys. The kin-openapi dependency is now a stable tagged release. Two P2 edge-case suggestions (multi-non-null type array handling and a nil-Responses guard for webhooks) do not block merging.

pkg/codegen/schema.go and pkg/codegen/operations.go have the P2 suggestions worth considering before a stable release.

Important Files Changed

Filename Overview
pkg/codegen/codegen.go Adds webhook/callback operation gathering to Generate(), wires globalState.is31 from the spec, and correctly gates webhook/callback output sections on their operation slices.
pkg/codegen/operations.go Adds WebhookOperationDefinitions and CallbackOperationDefinitions; both use SortedMapKeys on every map range, fixing the determinism concern from the earlier review. Nil guards are consistent with the existing path-operation code.
pkg/codegen/schema.go Adds schemaIsNullable, schemaPrimaryType, detectEnumViaOneOf, and describeWithExamples helpers; all depend on globalState.is31 being set before any call (precondition documented). schemaPrimaryType has an edge case for multi-non-null type arrays.
pkg/codegen/merge_schemas.go Switches ExclusiveMin/ExclusiveMax comparison to reflect.DeepEqual and nullable comparison to schemaIsNullable(), making merging version-aware without breaking 3.0 behavior.
pkg/codegen/configuration.go Adds SkipEnumViaOneOf opt-out flag; properly reflected in configuration-schema.json and the detectEnumViaOneOf gate.
pkg/codegen/templates/webhook-initiator.tmpl New template mirrors the existing client template; uses a per-call targetURL instead of a stored server URL, correct for the webhook dispatch model.
go.mod Updates kin-openapi to v0.136.0 (now a stable release), bumps Go to 1.26.2; the previously noted pseudo-version concern has been addressed.

Reviews (2): Last reviewed commit: "Fix lint issues and CI versioning" | 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
Copy link
Copy Markdown
Member

@jamietanna jamietanna left a comment

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!)

Copy link
Copy Markdown
Member

@jamietanna jamietanna left a comment

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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
mromaszewicz and others added 14 commits May 3, 2026 19:57
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…/<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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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
Copy link
Copy Markdown

zelch commented May 8, 2026

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.
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