oapi-codegen icon indicating copy to clipboard operation
oapi-codegen copied to clipboard

oneOf response struct

Open tandem97 opened this issue 1 year ago • 12 comments

 responses:
        '200':
          description: Order result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/orderResponseAck'
                  - $ref: '#/components/schemas/orderResponseResult'
                  - $ref: '#/components/schemas/orderResponseFull'

genrating into

type PostFapiV1Order200JSONResponse struct {
	union json.RawMessage
}

How should I fill and return this struct in strict server handler? Field union unexported.

tandem97 avatar Jun 21 '23 09:06 tandem97

From a project I've done this in before, I get the following generated:

// AsJSONPatchRequestAddReplaceTest returns the union data inside the PatchRequest_Item as a JSONPatchRequestAddReplaceTest
func (t PatchRequest_Item) AsJSONPatchRequestAddReplaceTest() (JSONPatchRequestAddReplaceTest, error) {
	var body JSONPatchRequestAddReplaceTest
	err := json.Unmarshal(t.union, &body)
	return body, err
}

// FromJSONPatchRequestAddReplaceTest overwrites any union data inside the PatchRequest_Item as the provided JSONPatchRequestAddReplaceTest
func (t *PatchRequest_Item) FromJSONPatchRequestAddReplaceTest(v JSONPatchRequestAddReplaceTest) error {
	b, err := json.Marshal(v)
	t.union = b
	return err
}

// MergeJSONPatchRequestAddReplaceTest performs a merge with any union data inside the PatchRequest_Item, using the provided JSONPatchRequestAddReplaceTest
func (t *PatchRequest_Item) MergeJSONPatchRequestAddReplaceTest(v JSONPatchRequestAddReplaceTest) error {
	b, err := json.Marshal(v)
	if err != nil {
		return err
	}

	merged, err := runtime.JsonMerge(b, t.union)
	t.union = merged
	return err
}

You should be able to use these to set the type based on your orderResponseAck, etc.

It's unfortunately not a super great solution (I wrote it 😅) but as we don't have union types in Go, this was the option we went for

jamietanna avatar Jun 29 '23 14:06 jamietanna

From a project I've done this in before, I get the following generated:

// AsJSONPatchRequestAddReplaceTest returns the union data inside the PatchRequest_Item as a JSONPatchRequestAddReplaceTest
func (t PatchRequest_Item) AsJSONPatchRequestAddReplaceTest() (JSONPatchRequestAddReplaceTest, error) {
	var body JSONPatchRequestAddReplaceTest
	err := json.Unmarshal(t.union, &body)
	return body, err
}

// FromJSONPatchRequestAddReplaceTest overwrites any union data inside the PatchRequest_Item as the provided JSONPatchRequestAddReplaceTest
func (t *PatchRequest_Item) FromJSONPatchRequestAddReplaceTest(v JSONPatchRequestAddReplaceTest) error {
	b, err := json.Marshal(v)
	t.union = b
	return err
}

// MergeJSONPatchRequestAddReplaceTest performs a merge with any union data inside the PatchRequest_Item, using the provided JSONPatchRequestAddReplaceTest
func (t *PatchRequest_Item) MergeJSONPatchRequestAddReplaceTest(v JSONPatchRequestAddReplaceTest) error {
	b, err := json.Marshal(v)
	if err != nil {
		return err
	}

	merged, err := runtime.JsonMerge(b, t.union)
	t.union = merged
	return err
}

You should be able to use these to set the type based on your orderResponseAck, etc.

It's unfortunately not a super great solution (I wrote it 😅) but as we don't have union types in Go, this was the option we went for

i cant access union field because I place generated code in separete package. So I need to write additional functions in this package that will fill union field with data?

tandem97 avatar Jun 29 '23 15:06 tandem97

No, you're not expected to manually interact with the union field, you should use the helper methods that are generated - hopefully PostFapiV1Order200JSONResponse should have other methods generated for it.

If not (and you're running latest oapi-codegen version) then it may be you need to move the type to /components/schemas to get the generation working, but that'll provide you helper methods like As..., From... and Merge... to make it possible

jamietanna avatar Jun 29 '23 15:06 jamietanna

Merge

openapi: 3.0.3

info:
  description: test server
  version: 1.0.0

paths:
  /fapi/v1/order:
    post:
      summary: New Order
      responses:
        '200':
          description: Order result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/orderResponseAck'
                  - $ref: '#/components/schemas/orderResponseResult'
                  - $ref: '#/components/schemas/orderResponseFull'
components:
  schemas:
    orderResponseAck:
      type: object
      properties:
        symbol:
          type: string

    orderResponseResult:
      type: object
      properties:
        orderId:
          type: integer
          format: int64
          example: 28

    orderResponseFull:
      type: object
      properties:
        orderListId:
          type: integer
          format: int64
          example: -1
//go:generate oapi-codegen --package api -o ./api/server.gen.go --generate chi-server,strict-server,spec,types ./api.yaml

here is test spec and generate command. I do not have any methods in PostFapiV1Order200JSONResponse

tandem97 avatar Jun 29 '23 15:06 tandem97

Merge

openapi: 3.0.3

info:
  description: test server
  version: 1.0.0

paths:
  /fapi/v1/order:
    post:
      summary: New Order
      responses:
        '200':
          description: Order result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/orderResponseAck'
                  - $ref: '#/components/schemas/orderResponseResult'
                  - $ref: '#/components/schemas/orderResponseFull'
components:
  schemas:
    orderResponseAck:
      type: object
      properties:
        symbol:
          type: string

    orderResponseResult:
      type: object
      properties:
        orderId:
          type: integer
          format: int64
          example: 28

    orderResponseFull:
      type: object
      properties:
        orderListId:
          type: integer
          format: int64
          example: -1
//go:generate oapi-codegen --package api -o ./api/server.gen.go --generate chi-server,strict-server,spec,types ./api.yaml

here is test spec and generate command. I do not have any methods in PostFapiV1Order200JSONResponse

Hi! Did you find a solution?

illiafox avatar Jul 11 '23 16:07 illiafox

Merge

openapi: 3.0.3

info:
  description: test server
  version: 1.0.0

paths:
  /fapi/v1/order:
    post:
      summary: New Order
      responses:
        '200':
          description: Order result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/orderResponseAck'
                  - $ref: '#/components/schemas/orderResponseResult'
                  - $ref: '#/components/schemas/orderResponseFull'
components:
  schemas:
    orderResponseAck:
      type: object
      properties:
        symbol:
          type: string

    orderResponseResult:
      type: object
      properties:
        orderId:
          type: integer
          format: int64
          example: 28

    orderResponseFull:
      type: object
      properties:
        orderListId:
          type: integer
          format: int64
          example: -1
//go:generate oapi-codegen --package api -o ./api/server.gen.go --generate chi-server,strict-server,spec,types ./api.yaml

here is test spec and generate command. I do not have any methods in PostFapiV1Order200JSONResponse

Hi! Did you find a solution?

No :(

tandem97 avatar Jul 12 '23 17:07 tandem97

Same here :(

sz-po avatar Jul 17 '23 23:07 sz-po

Ive tried to make schema object as such



openapi: 3.0.3

info:
  description: test server
  version: 1.0.0

paths:
  /tickets/{ticket_id}/accept:
    post:
      operationId: acceptTicket
      security:
        - RoleAuth:
          - admin
      summary: Accept a requested or reapplied Account Ticket
      parameters:
        - $ref: '#/components/parameters/ticketIdParam'
      tags:
        - Account Tickets
      responses:
        '200':
          description: Returns Account if the ticket was Reapply otherwise the AccountTicket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptTicketResponse'
components:
  schemas:
      AcceptTicketResponse:
        oneOf:
          - $ref: '#/components/schemas/Account'
          - $ref: '#/components/schemas/AccountTicket'
    Account:
      properties:
        id:
          type: integer
   AccountTicket:
      properties:
        ticket_id:
          type: integer

It generates ok code, but the decoder for VisitAcceptTicket does not decode the union but the type itself, returning an empty object.

Generated code:

type AcceptTicket200JSONResponse AcceptTicketResponse

// AcceptTicketResponse defines model for AcceptTicketResponse.
type AcceptTicketResponse struct {
	union json.RawMessage 
}

// AcceptTicketResponse0 defines model for .
type AcceptTicketResponse0 struct {
	Name string `json:"name"`
}

// AcceptTicketResponse1 defines model for .
type AcceptTicketResponse1 struct {
	Name2 string `json:"name2"`
}

// AsAcceptTicketResponse0 returns the union data inside the AcceptTicketResponse as a AcceptTicketResponse0
func (t AcceptTicketResponse) AsAcceptTicketResponse0() (AcceptTicketResponse0, error) {
	var body AcceptTicketResponse0
	err := json.Unmarshal(t.union, &body)
	return body, err
}

// FromAcceptTicketResponse0 overwrites any union data inside the AcceptTicketResponse as the provided AcceptTicketResponse0
func (t *AcceptTicketResponse) FromAcceptTicketResponse0(v AcceptTicketResponse0) error {
	b, err := json.Marshal(v)
	t.union = b
	return err
}

// MergeAcceptTicketResponse0 performs a merge with any union data inside the AcceptTicketResponse, using the provided AcceptTicketResponse0
func (t *AcceptTicketResponse) MergeAcceptTicketResponse0(v AcceptTicketResponse0) error {
	b, err := json.Marshal(v)
	if err != nil {
		return err
	}

	merged, err := runtime.JsonMerge(t.union, b)
	t.union = merged
	return err
}

// AsAcceptTicketResponse1 returns the union data inside the AcceptTicketResponse as a AcceptTicketResponse1
func (t AcceptTicketResponse) AsAcceptTicketResponse1() (AcceptTicketResponse1, error) {
	var body AcceptTicketResponse1
	err := json.Unmarshal(t.union, &body)
	return body, err
}

// FromAcceptTicketResponse1 overwrites any union data inside the AcceptTicketResponse as the provided AcceptTicketResponse1
func (t *AcceptTicketResponse) FromAcceptTicketResponse1(v AcceptTicketResponse1) error {
	b, err := json.Marshal(v)
	t.union = b
	return err
}

// MergeAcceptTicketResponse1 performs a merge with any union data inside the AcceptTicketResponse, using the provided AcceptTicketResponse1
func (t *AcceptTicketResponse) MergeAcceptTicketResponse1(v AcceptTicketResponse1) error {
	b, err := json.Marshal(v)
	if err != nil {
		return err
	}

	merged, err := runtime.JsonMerge(t.union, b)
	t.union = merged
	return err
}

and:

func (response AcceptTicket200JSONResponse) VisitAcceptTicketResponse(w http.ResponseWriter) error {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)

	return json.NewEncoder(w).Encode(response)
}

but only works if:

func (response AcceptTicket200JSONResponse) VisitAcceptTicketResponse(w http.ResponseWriter) error {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)

	return json.NewEncoder(w).Encode(response.union)
}

marmiha avatar Sep 19 '23 21:09 marmiha

It seems to be a problem when you have a content of response of type schema oneOf:

Foo:
  schema:
     oneOf:
        - ref$: 'obj1'
        - ref$: 'obj2'

Generates responses as:

type Foo200JSONResponse FooResponse

type FooResponse {
   union json.RawMessage
}

which decodes as {} when:

func (response  Foo200JSONResponse) VisitAcceptTicketResponse(w http.ResponseWriter) error {
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(200)
  
  return json.NewEncoder(w).Encode(response)
}

is called.

It would work if we would encode response.union and not response itself, as the encoding the response returns {} and not the content of the union.

Proposal that if the response has oneOf schema, then encode the response.union and not response as the 200 object is a type and not a struct.

func (response  Foo200JSONResponse) VisitAcceptTicketResponse(w http.ResponseWriter) error {
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(200)
  
  return json.NewEncoder(w).Encode(response.union)
}

marmiha avatar Sep 19 '23 22:09 marmiha

The same situation happens when you are using oneOf in request too

tandem97 avatar Sep 28 '23 15:09 tandem97

This can be solved in the same way as proto, interfaces and type switches.

Proto makes each of the oneof structs implement a noop interface, then provides getters and setters. Upon decoding you'd require that people provide a discriminator and that would allow you to generate each struct with a type switch.

pbdeuchler avatar Oct 17 '23 23:10 pbdeuchler

I modified template strict-interface.tmpl to insert union helpers:

From this line https://github.com/deepmap/oapi-codegen/blob/master/pkg/codegen/templates/strict/strict-interface.tmpl#L52, changes to:

            {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}}
                type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}}

                {{/* HACK: generate union helpers for response types */ -}}
                {{$discriminator := .Schema.Discriminator}}
                {{$properties := .Schema.Properties -}}
                {{range .Schema.UnionElements}}
                    {{$element := . -}}
                    // As{{ .Method }} returns the union data inside the {{$receiverTypeName}} as a {{.}}
                    func (t {{$receiverTypeName}}) As{{ .Method }}() ({{.}}, error) {
                        var body {{.}}
                        err := json.Unmarshal(t.union, &body)
                        return body, err
                    }

                    // From{{ .Method }} overwrites any union data inside the {{$receiverTypeName}} as the provided {{.}}
                    func (t *{{$receiverTypeName}}) From{{ .Method }} (v {{.}}) error {
                        {{if $discriminator -}}
                            {{range $value, $type := $discriminator.Mapping -}}
                                {{if eq $type $element -}}
                                    {{$hasProperty := false -}}
                                    {{range $properties -}}
                                        {{if eq .GoFieldName $discriminator.PropertyName -}}
                                            t.{{$discriminator.PropertyName}} = "{{$value}}"
                                            {{$hasProperty = true -}}
                                        {{end -}}
                                    {{end -}}
                                    {{if not $hasProperty}}v.{{$discriminator.PropertyName}} = "{{$value}}"{{end}}
                                {{end -}}
                            {{end -}}
                        {{end -}}
                        b, err := json.Marshal(v)
                        t.union = b
                        return err
                    }

                    // Merge{{ .Method }} performs a merge with any union data inside the {{$receiverTypeName}}, using the provided {{.}}
                    func (t *{{$receiverTypeName}}) Merge{{ .Method }} (v {{.}}) error {
                        {{if $discriminator -}}
                            {{range $value, $type := $discriminator.Mapping -}}
                                {{if eq $type $element -}}
                                    {{$hasProperty := false -}}
                                    {{range $properties -}}
                                        {{if eq .GoFieldName $discriminator.PropertyName -}}
                                            t.{{$discriminator.PropertyName}} = "{{$value}}"
                                            {{$hasProperty = true -}}
                                        {{end -}}
                                    {{end -}}
                                    {{if not $hasProperty}}v.{{$discriminator.PropertyName}} = "{{$value}}"{{end}}
                                {{end -}}
                            {{end -}}
                        {{end -}}
                        b, err := json.Marshal(v)
                        if err != nil {
                        return err
                        }

                        merged, err := runtime.JSONMerge(t.union, b)
                        t.union = merged
                        return err
                    }
                {{end}}

Or in other way, you can describe the schema like this:

responses:
        '200':
          description: Order result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/orderResponse'
components:
  securitySchemes: {}
  schemas:
    orderResponse:
      oneOf:
        - $ref: '#/components/schemas/orderResponseAck'
        - $ref: '#/components/schemas/orderResponseResult'
        - $ref: '#/components/schemas/orderResponseFull'

This will generate response object with union, and OrderResponse with union and helpers. Then just cast OrderResponse to response object before returning from handler.

davy-ikv avatar Mar 06 '24 07:03 davy-ikv