OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.37k stars 6.46k forks source link

Improve handling of oneOf #15

Open jmini opened 6 years ago

jmini commented 6 years ago

With OAS3 it is possible to use oneOf. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schema-object

See one example in composed-oneof.yaml from the test suite.

We should discuss how we want to handle this.

In my opinion (for java) the Schema containing only oneOf entries should be an interface, and all model classes corresponding to the schema mentioned in the oneOf should implement this interface.

jonschoning commented 6 years ago

What should DefaultCodegen expose?

Currently DefaultCodegen simply exposes a simple string returnType, so we don't know what the possible values are. Although overriding fromOperation would give access to Map<String, Schema> schemas but not sure if implementations should use schemas directly or expect a certain field on DefaultCodegen to assist in determing the possible values.

https://github.com/OpenAPITools/openapi-generator/blob/23ad9f393759cfc32d001532d23b524122bac4b5/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java#L2165

changing returnType would probably break a lot of templates, so maybe there is a way for a lang to opt-in to supporting `oneOf``

just some thoughts, no real preferences on this atm

jmini commented 6 years ago

Well I think that in Java, an interface should be created, because you have no way to express a Type Union: return type is ObjA or ObjB.

Yes we need to add this to the Codegen without breaking anything for existing templates. I did not investigate yet, how this could be added.

jonschoning commented 6 years ago

For Java, what would the interface look like for composed-oneof.yaml from the test suite? (ObjA and ObjB have different properties, and in theory could.have no properties in common). Also is the realtype field going to be a requirement to make it work?

jmini commented 6 years ago

We have problem with names when schema are inlined (this should be addressed in #8). Let call it CompObj.


The interface can be empty in my opinion. The idea is that in Java code, you need to check with instanceOf: With obj being a CompObj

if(obj instanceOf ObjA) {
...
} else if(obj instanceOf ObjB) {
...
}

In typescript you do not need CompObj you can work directly with Union Type: http://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types


About the discriminator (realtype)... It is not mandatory to have it in ObjA and ObjB. I do not think that it should be defined in the interface, but it will be useful information for the serializer/deserializer.

jonschoning commented 6 years ago

ah, yes, an empty/marker interface + casting would work. The old-school way of doing tagged unions in langs that don't support them is to expose a struct with a discriminator/enum that identifies which field contains the data e.g.

(pseudo-code)
[struct]
{
   discriminator: Enum/Int
   objA: ObjA
   objB: ObjB
}

which is more static in that it avoids casting, but has downside of having to use the discriminator to get the right data. But I'm not involved enough with Java to know what the best practices for Java are.

The marker interface method may be a cleaner solution.

Also worth noting other tools like c# autorest I don't think support this currently.

jmini commented 6 years ago

I think we should published the necessary information in the Codegen layer, each template can implement in its own way (depending on the capabilities / language features)

lorenzleutgeb commented 6 years ago

I duplicated this as #475, and came to the same conclusion as @jmini in the previous comment. Codegen should expose as much information as possible to the generators, and they should use language features accordingly.

jeff9finger commented 6 years ago

My company requires oneOf functionality.

developerpat commented 5 years ago

Is there a bugfix at the actual Version? I am using Version 3.3.2 and i have the same problem with oneOf.

jmini commented 5 years ago

I think I will give the marker-interface approach a try in the Java-client generators:

For those schemas:

components:
    schemas:
        MainObj:
            type: object
            oneOf:
                - $ref: '#/components/schemas/ObjA'
                - $ref: '#/components/schemas/ObjB'
            discriminator:
                propertyName: realtype
                mapping:
                    a-type: '#/components/schemas/ObjA'
                    b-type: '#/components/schemas/ObjB'
        ObjA:
            type: object
            properties:
                realtype:
                    type: string
                message:
                    type: string
        ObjB:
            type: object
            properties:
                realtype:
                    type: string
                description:
                    type: string
                code:
                    type: integer
                    format: int32

I would generate:

wing328 commented 5 years ago

@jmini sounds good to me 👍

jmini commented 5 years ago

Interesting case in oneOf.yaml:

    fruit:
      title: fruit
      type: object
      properties:
        color:
          type: string
      oneOf:
        - $ref: '#/components/schemas/apple'
        - $ref: '#/components/schemas/banana'

It seems to be possible to add properties and oneOf in the same schema. I am not sure what the semantic is in this case, but we might need to support this as well.

The interface pattern that I have described here, only works for Schema with only oneOf (meaning without properties)

SaratKumarM commented 5 years ago

Do the OpenApi-Generator support OneOf / Any-Of combinations?

clojj commented 5 years ago

What is the status of "oneOf" issues ? We'd like to generate from a spec which has several oneOf... not only as requestBody, but also as responseBody

tomghyselinck commented 5 years ago

See also #2121 for Python Client support

rienafairefr commented 5 years ago

Interesting case in oneOf.yaml:

    fruit:
      title: fruit
      type: object
      properties:
        color:
          type: string
      oneOf:
        - $ref: '#/components/schemas/apple'
        - $ref: '#/components/schemas/banana'

It seems to be possible to add properties and oneOf in the same schema. I am not sure what the semantic is in this case, but we might need to support this as well.

The interface pattern that I have described here, only works for Schema with only oneOf (meaning without properties)

@jmini I think json schema in this case specifies that there is an implicit allOf combining all these, but I can't seem to find the place where I read this. The spec would be equivalent to:

fruit_color:
  type: object
  properties:
    color:
      type: string
fruit_w_type:
  type: object
  oneOf:
    - $ref: '#/components/schemas/apple'
    - $ref: '#/components/schemas/banana'
fruit:
  title: fruit
  type: object
  allOf:
    - $ref: '#/components/schemas/fruit_color'
    - $ref: '#/components/schemas/fruit_w_type'
alfirin commented 5 years ago

Any news on this?

When I use oneOf in the Swagger.json specification file, I'll get these errors:

[ERROR] generated-code/spring/src/main/java/com/se/edm/model/Network.java:[312,29] cannot find symbol
[ERROR]   symbol:   class OneOfModbusSLNetworkParameterModbusTCPNetworkParameterZigbeeNetworkParameter
[ERROR]   location: class com.se.edm.model.Network
jack-ev commented 5 years ago

The same for generated c# client. Oneof property typed as "Oneof..." in output project instead of using object (as it was in openapi-generator-tool of 3.3.4 version)

MaPhiWe commented 5 years ago

Could you share what the status is?

I have a Swagger file containing oneOf.

` bankAccount: oneOf:

It compiles to

public static final String JSON_PROPERTY_BANK_ACCOUNT = "bankAccount"; private OneOfBankAccountWithIbanBankAccountWithoutIban bankAccount = null;

but OneOfBankAccountWithIbanBankAccountWithoutIban is undefined

Thanks a lot!

janssk1 commented 5 years ago

oneOf just means that one of the 'subschema' should match. Those schema's can even be validation-only schemas, without any properties. 3GPP is using 'oneOf' to express that one of two properties in the 'parent' must be required.

Example:

`components: schemas: Notification: type: object properties: externalId: type: string msisdn: type: string data: type: string required:

When i'm running swagger codegen on the above snippet, i'm getting an empty class (no properties).

For reference, the full 3gpp spec containing oneOf's: http://www.3gpp.org/ftp//Specs/archive/29_series/29.122/29122-f40.zip

MaPhiWe commented 5 years ago

Thanks. My problem is that the class OneOfBankAccountWithIbanBankAccountWithoutIban is used as a type, but not created anywhere, causing the compilation to break.

The obvious workaround (declare the empty class in the regular code) is available.

realvictorprm commented 4 years ago

I'm amused, so far I see no code generator produces correct output for oneOf, right? :joy:

Now for real, is there any codegen producing correct output out there? I'ld like to know :smiley:

jonschoning commented 4 years ago

@realvictorprm I don't know about the dynamic langs, but for the static langs I think the issue is that trying to encode different result schemas in the typesystem could involve a lot of boilerplate, and each language would have to solve this in it's own way - and I think template authors are reluctant to force a lot of boilerplate or special casing on their users.

Do you have any ideas to contribute on the approach in general?

I'm not even sure if everyone has the same goals with safety/strictness vs convenience.

For example, I think it was proposed for Java the return type would be Object and the user would be forced to downcast to the appropriate type, as one solution.

jonschoning commented 4 years ago

Also, are there changes needed in the core generator to support oneOf, or is it only work that needs to be done by template authors for this?

amitinfo2k commented 4 years ago

I tried GoGenerator on the following:

https://github.com/jdegre/5GC_APIs/blob/master/TS29509_Nausf_UEAuthentication.yaml

which uses 'oneof' as follows

...
        5gAuthData:
          oneOf:
            - $ref: '#/components/schemas/Av5gAka'
            - $ref: '#/components/schemas/EapPayload'
...

The code is getting generated but, while execution it is not able to resolved oneof parameter. Is there any workaround ?

bkabrda commented 4 years ago

Hey folks 👋 I took a shot at implementing this for Java jackson clients in #4785 - I'd be glad for any feedback and/or additional testing of my code. Thanks!

mmalygin commented 4 years ago

The same issue with "spring" generator. Is it possible to implement the same behavior which @bkabrda implemented for client generator in https://github.com/OpenAPITools/openapi-generator/pull/4785?

mmalygin commented 4 years ago

I did it for Spring and all JaxRS server generators in my fork. See https://github.com/mmalygin/openapi-generator/commit/3303da5e39dcd2d5b5812ef73452ded0b9d413b8 To enable the feature, set useOneOfInterfaces=true in additional properties.

jburgess commented 4 years ago

@mmalygin this relates/overlaps with https://github.com/OpenAPITools/openapi-generator/issues/5381. Do you plan on creating a PR for this work?

@bkabrda @jimschubert - Do you have a view of which implementation best aligns with the merged changes from https://github.com/OpenAPITools/openapi-generator/pull/4785? I think it is imperative that we have alignment across all flavors of clients and server generators.

ghost commented 4 years ago

I have the same problem when generating code from a spec containing oneOf, for QT5/C++.

A file "OneOf..." header file is included, but not generated anywhere (at least with version 4.3.0).

dnoliver commented 4 years ago

As @amitinfo2k. I also tried the go generator with the Video Analytics Serving OpenAPI

https://github.com/intel/video-analytics-serving/blob/v0.3.0-alpha/vaserving/rest_api/video-analytics-serving.yaml

It uses oneOf as well

      properties:
        source:
          discriminator:
            propertyName: type
          oneOf:
          - $ref: '#/components/schemas/URISource'
          - $ref: '#/components/schemas/DeviceSource'
          type: object
        destination:
          discriminator:
            propertyName: type
          oneOf:
          - $ref: '#/components/schemas/KafkaDestination'
          - $ref: '#/components/schemas/MQTTDestination'
          - $ref: '#/components/schemas/FileDestination'
          type: object

I used the docker generator for it:

$ docker images | grep openapi-generator-cli
openapitools/openapi-generator-cli                        latest                                     c03abe67cb2d        3 hours ago         135MB

$ docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/intel/video-analytics-serving/v0.3.0-alpha/vaserving/rest_api/video-analytics-serving.yaml -g go -o /local/out/go

Then using it in a client throws errors:

$ go run client.go 
# openapi
../../go/src/openapi/model_pipeline_request.go:13:9: undefined: OneOfUriSourceDeviceSource
../../go/src/openapi/model_pipeline_request.go:14:14: undefined: OneOfKafkaDestinationMqttDestinationFileDestination

The generated go code look like this:

package openapi
// PipelineRequest struct for PipelineRequest
type PipelineRequest struct {
    Source OneOfUriSourceDeviceSource `json:"source,omitempty"`
    Destination OneOfKafkaDestinationMqttDestinationFileDestination `json:"destination,omitempty"`
    // Client specified values. Returned with results.
    Tags map[string]interface{} `json:"tags,omitempty"`
    // Pipeline specific parameters.
    Parameters map[string]interface{} `json:"parameters,omitempty"`
}

Is there any fix I can test for this?

wing328 commented 4 years ago

Please try the latest go-experimental generator, which has better support for oneOf and anyOf.

dnoliver commented 4 years ago

Tried the go-experimental one:

docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/intel/video-analytics-serving/v0.3.0-alpha/vaserving/rest_api/video-analytics-serving.yaml -g go-experimental -o /local/out/go

Still have problems with the oneOf attr:

test@ubuntu1804-2:~/Downloads/hello-go$ go run client.go 
# _/home/test/Downloads/hello-go/openapi
openapi/model_pipeline_request.go:18:10: undefined: OneOfURISourceDeviceSource
openapi/model_pipeline_request.go:19:15: undefined: OneOfKafkaDestinationMQTTDestinationFileDestination
openapi/model_pipeline_request.go:44:39: undefined: OneOfURISourceDeviceSource
openapi/model_pipeline_request.go:46:11: undefined: OneOfURISourceDeviceSource
openapi/model_pipeline_request.go:54:43: undefined: OneOfURISourceDeviceSource
openapi/model_pipeline_request.go:71:39: undefined: OneOfURISourceDeviceSource
openapi/model_pipeline_request.go:76:44: undefined: OneOfKafkaDestinationMQTTDestinationFileDestination
openapi/model_pipeline_request.go:78:11: undefined: OneOfKafkaDestinationMQTTDestinationFileDestination
openapi/model_pipeline_request.go:86:48: undefined: OneOfKafkaDestinationMQTTDestinationFileDestination
openapi/model_pipeline_request.go:103:44: undefined: OneOfKafkaDestinationMQTTDestinationFileDestination
openapi/model_pipeline_request.go:78:11: too many errors

The generated code is similar to the previous one:

package openapi

import (
    "encoding/json"
)

// PipelineRequest struct for PipelineRequest
type PipelineRequest struct {
    Source *OneOfURISourceDeviceSource `json:"source,omitempty"`
    Destination *OneOfKafkaDestinationMQTTDestinationFileDestination `json:"destination,omitempty"`
    // Client specified values. Returned with results.
    Tags *map[string]interface{} `json:"tags,omitempty"`
    // Pipeline specific parameters.
    Parameters *map[string]interface{} `json:"parameters,omitempty"`
}
cljk commented 4 years ago

Is there not unit-test for retrofit2 generator using oneOf?

For me the generated code does not even compile...

My schema

        user_profile_properties:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/UserProfileTextProperty'
              - $ref: '#/components/schemas/UserProfileImageProperty'
              - $ref: '#/components/schemas/UserProfileSelectionProperty'
            discriminator:
              propertyName: type
              mapping:
                text: '#/components/schemas/UserProfileTextProperty'
                image: '#/components/schemas/UserProfileImageProperty'
                selection: '#/components/schemas/UserProfileSelectionProperty'

The compile-error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:compile (default-compile) on project project-api-client-retrofit2: Compilation failure: Compilation failure:
[ERROR] /C:/Users/me/workspaces/work/project/project-api-client/project-api-client-retrofit2/target/generated-sources/openapi/src/main/java/net/worke/project/restclient/model/SystemInformation.java:[30,39] cannot find
symbol
[ERROR]   symbol:   class OneOfUserProfileTextPropertyUserProfileImagePropertyUserProfileSelectionProperty
[ERROR]   location: package net.worke.project.restclient.model
[ERROR] /C:/Users/me/workspaces/work/project/project-api-client/project-api-client-retrofit2/target/generated-sources/openapi/src/main/java/net/worke/project/restclient/model/SystemInformation.java:[75,16] cannot find
symbol
[ERROR]   symbol:   class OneOfUserProfileTextPropertyUserProfileImagePropertyUserProfileSelectionProperty
[ERROR]   location: class net.worke.project.restclient.model.SystemInformation
[ERROR] /C:/Users/me/workspaces/work/project/project-api-client/project-api-client-retrofit2/target/generated-sources/openapi/src/main/java/net/worke/project/restclient/model/SystemInformation.java:[301,55] cannot find
 symbol

The generated model class SystemInformation is referencing a class OneOfUserProfileTextPropertyUserProfileImagePropertyUserProfileSelectionProperty which is not there.

  public static final String SERIALIZED_NAME_USER_PROFILE_PROPERTIES = "user_profile_properties";
  @SerializedName(SERIALIZED_NAME_USER_PROFILE_PROPERTIES)
  private List<OneOfUserProfileTextPropertyUserProfileImagePropertyUserProfileSelectionProperty> userProfileProperties = null;

There is only UserProfileImageProperty, UserProfileTextPropertyand UserProfileSelectionProperty.

I checked the behaviour also for type: object instead of type: array - same (comparable) error.

wing328 commented 4 years ago

@cljk retrofit2 doesn't have oneOf/anyOf support. Of course we welcome contributions to support that.

Please try jersey2 instead which has better support for oneOf/anyOf.

cljk commented 4 years ago

@wing328 Perhaps my definition/usage of oneOf was not correct. Even after consuming the OpenAPI doc several times I´m not quite sure. I modified my schema a bit and replaced it with usage of allOfand it now even works in retrofit2. Instead of defining my property user_profile_properties as oneOf I now defined a super type which has at least the discriminator as field. Then in the subtypes I referenced it with allOf. This leads to the generation of a super class and my sub classes. Processing/parsing tested successfully so far in jersey and retrofit2 client adapters.

OLD

        user_profile_properties:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/UserProfileTextProperty'
              - $ref: '#/components/schemas/UserProfileImageProperty'
              - $ref: '#/components/schemas/UserProfileSelectionProperty'
            discriminator:
              propertyName: type
              mapping:
                text: '#/components/schemas/UserProfileTextProperty'
                image: '#/components/schemas/UserProfileImageProperty'
                selection: '#/components/schemas/UserProfileSelectionProperty

NEW

       user_profile_properties:
          type: array
          items:
            $ref: '#/components/schemas/UserProfileProperty'

    UserProfileProperty:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          # enum: [text, image, selection]
        name:
          type: string
      discriminator:
        propertyName: type
        mapping:
          text: '#/components/schemas/UserProfileTextProperty'
          email: '#/components/schemas/UserProfileEmailProperty'

    UserProfileTextProperty:
      allOf:
        - $ref: '#/components/schemas/UserProfileProperty'
        - type: object
          properties:
            multiline:
              type: boolean
            required:
              type: boolean

    UserProfileEmailProperty:
      allOf:
        - $ref: '#/components/schemas/UserProfileProperty'
        - type: object
          properties:
            required:
              type: boolean

Spoiler: the discriminator as enum does not work...

leonluc-dev commented 3 years ago

Is there any way to make oneOf, anyOf, allOf etc. work in de ASP.NET Core server stub generator? We don't have full control of the OpenAPI document we have to auto-generate code for (with the only guarantee being that the document adheres to the 3.0 spec)

Recently the document we have to adhere to started using oneOf and anyOf. Whenever the document contains oneOf/anyOf validators like this:

"responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "oneOf": [
                {
                  "$ref": "#/components/schemas/CustomerModel"
                },
                {
                  "$ref": "#/components/schemas/ProjectModel"
                }
            ]
          }
        }
      }
}

the attribute generated references a class named OneOfProjectModelCustomerModel which doesn't exist.

[ProducesResponseType(statusCode: 200, type: typeof(OneOfProjectModelCustomerModel))]

The same happens for models like this:

"TestDataModel": {
    "type": "object",
    "properties": {
        "testValue": {
            "oneOf": [
              { 
                "type": "string" 
              },
              {
                "$ref": "#/components/schemas/CustomerModel"
              }
            ],
            "nullable": true
          }
    }
}

Will generate the following uncompilable property:

[DataMember(Name="testValue", EmitDefaultValue=true)]
public OneOfstringCustomerModel TestValue{ get; set; }

Is there a way to "fix" this using the generator (since we can't change the OpenAPI doc) or would we have to change the generated code (example: by replacing these classes with generic .NET "object" references)?

javo8a commented 3 years ago

I have a similar use of the generator for netcore as your first example. The difference is that in the path response I have a $ref then the $ref has the oneOf in it. Something like this:

        '200':
          description: Ok
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/VPublic'

and the schema like this:

VPublic:
      oneOf:
        - $ref: '#/components/schemas/VPublicSR'
        - $ref: '#/components/schemas/VPublicVR'
        - $ref: '#/components/schemas/VPublicIMVI'
        - $ref: '#/components/schemas/VPublicOSI'

this creates a DTO named VPublic that has a combination of all fields across the subschemas.

So I guess for you that won't be an answer since you can't manipulate the spec you use? I have no idea how to do this otherwise.

For your second example I haven't tried anything like that. But I read that in the new spec Openapi 3.1 they introduced the polymorphism when defining types as an array... ["string","null"] or something like that. Of course that is the spec the tooling is not there yet :)

spacether commented 2 years ago

For context here, oneOf can be combined with any of the other openapi keywords. One can have oneOf anyOf and allOf. Or properties and oneOf Or items and oneOf or a type constraint and OneOf etc. This issue's question is one specific common use case, not the general use case. Python supports all of the mentioned general cases. One can see this working for a model that combines allOf/anyOf/oneOf, and the test of it here.