Skip to content

Bind styled parameters into any destinations using a declared Types list (OpenAPI 3.1 multi-type unions) #153

Description

@mromaszewicz

Motivation

oapi-codegen/oapi-codegen#2521 / oapi-codegen/oapi-codegen#2522 map OpenAPI 3.1 multi-type unions (type: [string, integer]) to any. That works for schema positions, but a union in a parameter position generates code that compiles and then fails on every request: the binder receives an *any destination and rejects it unconditionally:

error binding string parameter: can not bind to destination of type: interface

The binding machinery is destination-driven — it reflects on the Go type to decide how to parse — and an interface destination carries no information. The schema knew the answer ([string, integer]), but the runtime never sees it: BindStyledParameterOptions.Type / BindQueryParameterOptions.Type exist and are populated by generated code, yet nothing consumes them today (only Format == "byte" is load-bearing). A styled value like 123 is also genuinely ambiguous — URL serialization is lossy — so the runtime needs the member list to make a deterministic choice.

Proposal

Add one field to both options structs:

// Types is the OpenAPI type list of the parameter for 3.1 multi-type
// unions (e.g. ["string", "integer"]), with "null" stripped. When
// non-empty it takes precedence over Type. It is only consulted when
// the destination is an interface; concrete destinations keep the
// existing reflection-driven path unchanged.
Types []string

Binding semantics when Types is non-empty and the destination is an interface:

  1. Try members in specificity orderboolean, integer, number, string — restricted to the members actually present. First successful parse wins.
    • Deliberately not schema-declaration order: JSON Schema defines the type array as an unordered set, and string always parses, so declaration order would make the string member swallow everything whenever it's listed first.
  2. Numeric detection uses JSON grammar, not strconv leniency: 007, +1, 1 are not JSON numbers and fall through to string. This makes the result identical to what json.Unmarshal into any would produce for the same token, which is the documented mental model.
  3. Format applies only to its host member and only where it changes decoding: byte base64-decodes the string member (yielding []byte); int32/int64/float/double select the numeric width. Annotation-only formats (date-time, uuid, …) are ignored in v1 — per 3.1 semantics format is an annotation and must not reject a value, so parse failure can't be a discriminator anyway. A format whose host type isn't in Types is silently inert (spec-legal).
  4. array / object members are out of scope and keep failing — styled serialization of those into any has no sensible meaning.
  5. If no member parses (e.g. Types: ["integer", "boolean"] and value abc), return a binding error naming the parameter and the member list.

Compatibility

  • Adding a struct field is API-compatible; old generated code never sets it.
  • Gating on interface destinations means no existing binding path changes behavior — concrete destinations are untouched even when Types is set.
  • Old runtime + new generated code fails at compile time (unknown field), the same coupling story as the recent Type/Format/ValueIsUnescaped additions.

Codegen side (tracked in the main repo)

  • Emit Types for multi-type params, "null" stripped, mirroring schemaPrimaryType.
  • Fix ParameterDefinition.SchemaType(), which currently emits the first entry of the type list (Type: "string" for [string, integer], and would emit "null" for ["null", "string"]). Harmless while Type is unread; a landmine once type metadata becomes load-bearing.
  • Client-side StyleParamWithOptions serialization of an any-valued union param needs a round-trip test in the same change.

Explicitly not this issue

A fully schema-driven binder (runtime honoring declared types over destination reflection everywhere) was considered and rejected for this repo: destination types already encode the schema's answer in the common case, type-mapping/x-go-type deliberately decouple the two and rely on reflection, and version skew across years of generated code means it could only ever be an opt-in second path. If Types+Format later prove insufficient (arrays of unions, explode ambiguities, deepObject), the natural promotion is a small runtime-owned ParamSchema{Types, Format, Items} descriptor on the same options structs — parser-agnostic, so it stays viable across kin-openapi and libopenapi — rather than accepting parser AST types into this module's API.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions