Skip to content

strict-server: response types silently drop data for allOf-composed unions and properties + additionalProperties #2549

Description

@PieterVoorwinden

PR #2332 fixed strict-server response types losing their model's MarshalJSON, closing #970 and #1665. Two shapes are still affected: the response is emitted as a defined type with no delegation, and encoding/json quietly serialises an incomplete body. No error, no compile failure — just a 200 with fields missing.

Environment

  • oapi-codegen v2.8.0
  • oapi-codegen/runtime v1.6.0
  • Go 1.26.4 (darwin/arm64)

Configuration

package: repro
generate:
  strict-server: true
  gorilla-server: true
  models: true
output-options:
  skip-prune: true

Minimal specification

openapi: "3.0.0"
info:
  title: allOf-composed union repro
  version: "1.0"
paths:
  /thing:
    get:
      operationId: getThing
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Thing" }
  /bag:
    get:
      operationId: getBag
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Bag" }
components:
  schemas:
    Base:
      type: object
      required: [id]
      properties:
        id: { type: string }
    # case 1: union nested inside an allOf member
    Thing:
      allOf:
        - $ref: "#/components/schemas/Base"
        - oneOf:
            - $ref: "#/components/schemas/Cat"
            - $ref: "#/components/schemas/Dog"
    Cat:
      type: object
      properties: { meow: { type: string } }
    Dog:
      type: object
      properties: { woof: { type: string } }
    # case 2: fixed properties alongside additionalProperties
    Bag:
      type: object
      required: [kind]
      properties:
        kind: { type: string }
      additionalProperties: true

Current behaviour

The models are generated correctly. Both get a custom marshaller:

type Thing struct {
	Id    string `json:"id"`
	union json.RawMessage
}
func (t Thing) MarshalJSON() ([]byte, error) { ... }

type Bag struct {
	Kind                 string                 `json:"kind"`
	AdditionalProperties map[string]interface{} `json:"-"`
}
func (a Bag) MarshalJSON() ([]byte, error) { ... }

The response types do not, so nothing delegates to those marshallers:

type GetThing200JSONResponse Thing   // no MarshalJSON
type GetBag200JSONResponse Bag       // no MarshalJSON

The generated visitor encodes the response value directly:

func (response GetThing200JSONResponse) VisitGetThingResponse(w http.ResponseWriter) error {
	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(response); err != nil {
		return err
	}
	...
}

Because GetThing200JSONResponse is a defined type it does not inherit Thing's method, so encoding/json falls back to struct-field encoding and drops the unexported union field. Same for Bag, whose AdditionalProperties map is tagged json:"-".

Serving each response through its generated visitor:

Response value Actual (v2.8.0) Expected
Thing{Id: "abc"} + FromCat(Cat{Meow: "purr"}) {"id":"abc"} {"id":"abc","meow":"purr"}
Bag{Kind: "demo", AdditionalProperties: {"extra": 42}} {"kind":"demo"} {"extra":42,"kind":"demo"}

Expected behaviour

The response type should delegate to the model's marshaller, exactly as #2332 already does for a response whose schema is directly a oneOf/anyOf.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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