Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Typescript] Generate oneOf schemas as type unions #19027

Conversation

ksvirkou-hubspot
Copy link
Contributor

@ksvirkou-hubspot ksvirkou-hubspot commented Jun 27, 2024

Description of the PR

I have implemented oneOf schemas as a type union and a oneOf class with a discriminator and mapping, similar to PR 2617 and PR 2647, but for the TypeScript generator.

The type union is used for methods like request/response types and for models as a type of parameters. The oneOf class is used for the deserialization process to determine the type of an object from the type union. It checks the discriminator in the mapping list, then in the list of all classes.

Before this PR, the TypeScript generator created a new model with all fields from all oneOf models. The created model is not described in the swagger files. There are the following problems:

The implementation "oneOf" schemas as a type union resolve all these problems, making the generated code accurate with the swagger specification.

Sample input:

"inputFieldDependencies" : {
    "type" : "array",
    "items" : {
        "oneOf" : [ {
            "$ref" : "#/components/schemas/PublicSingleFieldDependency"
        }, {
            "$ref" : "#/components/schemas/PublicConditionalSingleFieldDependency"
        } ],
        "discriminator": {
            "propertyName": "dependencyType",
            "mapping": {
                "SINGLE_FIELD": "#/components/schemas/PublicSingleFieldDependency",
                "CONDITIONAL_SINGLE_FIELD": "#/components/schemas/PublicConditionalSingleFieldDependency"
            }
        }
    }
},

Sample output:

import { PublicConditionalSingleFieldDependency } from '../models/PublicConditionalSingleFieldDependency';
import { PublicSingleFieldDependency } from '../models/PublicSingleFieldDependency';

export type PublicActionDefinitionInputFieldDependenciesInner = PublicConditionalSingleFieldDependency | PublicSingleFieldDependency;

export class PublicActionDefinitionInputFieldDependenciesInnerClass {
    static readonly discriminator: string | undefined = "dependencyType";

    static readonly mapping: {[index: string]: string} | undefined = {
        "CONDITIONAL_SINGLE_FIELD": "PublicConditionalSingleFieldDependency",
        "SINGLE_FIELD": "PublicSingleFieldDependency",
    };
}

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package 
    ./bin/generate-samples.sh ./bin/configs/*.yaml
    ./bin/utils/export_docs_generators.sh
    
  • File the PR against the correct branch: master (upcoming 7.6.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Related issues:

#9305

@ksvirkou-hubspot
Copy link
Contributor Author

@@ -12,7 +12,7 @@ import { HttpFile } from '../http/http{{importFileExtension}}';
*/
{{/description}}
{{^isEnum}}
export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{
{{#oneOf}}{{#-first}}{{>model/modelOneOf}}{{/-first}}{{/oneOf}}{{^oneOf}}export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{{#oneOf}}{{#-first}}{{>model/modelOneOf}}{{/-first}}{{/oneOf}}{{^oneOf}}export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{
{{#oneOf}}
{{#-first}}{{>model/modelOneOf}}{{/-first}}
{{/oneOf}}
{{^oneOf}}
export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{

'bark'?: boolean;
'breed'?: PetsPatchRequestBreedEnum;

static readonly discriminator: string | undefined = "petType";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes the discriminator value to undefined, since we have

static readonly discriminator: string | undefined = undefined;

is this intentional?

Copy link
Contributor Author

@ksvirkou-hubspot ksvirkou-hubspot Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, intentional
I've created type and class:

/**
 * @type PetsPatchRequest
 * Type
 * @export
 */
export type PetsPatchRequest = Cat | Dog;

/**
* @type PetsPatchRequestClass
* @export
*/
export class PetsPatchRequestClass {
        static readonly discriminator: string | undefined = "petType";
        
        static readonly mapping: {[index: string]: string} | undefined = undefined;
}

https://github.com/OpenAPITools/openapi-generator/pull/19027/files#diff-531a756a09a9bb02d3916047a7b0c39dc8caf49c638c9c74391ad432166cd579R25

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not seem to be backwards compatible, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, because now it isn't work at all
Should I make a PR to 8.0.x?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you are saying the currently generated classes (before this PR) for the typescript generator dont work? what does not work?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just so i correctly understand, can you update the PR description with the motivation why this change is needed, what use case it solves, and why there are two types generated (the union and the class), and why the class does not have any fields besides a mapping and discriminator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the PR description.
Could you check it, please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any updates?
@macjohnny

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any updates?
@macjohnny @wing328

Copy link
Member

@macjohnny macjohnny Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check soon. Sorry, for the delay

@wing328
Copy link
Member

wing328 commented Jul 10, 2024

Generate oneOf schemas as type unions

@ksvirkou-hubspot given that this implementation is for oneOf, will this implementation throw an error if the JSON payload for example matches both models/schemes (e.g. Pet, Animal) defined in oneOf?

@ksvirkou-hubspot
Copy link
Contributor Author

ksvirkou-hubspot commented Jul 25, 2024

Generate oneOf schemas as type unions

@ksvirkou-hubspot given that this implementation is for oneOf, will this implementation throw an error if the JSON payload for example matches both models/schemes (e.g. Pet, Animal) defined in oneOf?

Let me clarify the way ... should work.
Discriminator is a field by which the generated code can determine what model is in the JSON payload.
Example of swagger:

oneOf:
  - $ref: '#/components/schemas/Cat'
  - $ref: '#/components/schemas/Dog'
  - $ref: '#/components/schemas/Lizard'
  discriminator:
    propertyName: petType

Example of payload:

{
  "id": 123,
  "petType": "Cat",
}

In this case : Cat model.
Considering what I've described, I don't think I fully understand the scenario you've meant. Could you share the swagger and the json payload to reproduce the case?

@ksvirkou-hubspot
Copy link
Contributor Author

Could you check it, please?
@macjohnny @wing328

@joscha
Copy link
Contributor

joscha commented Aug 30, 2024

I ran into a similar issue in planet-a-ventures/affinity-node#35 and was passed on this PR by @macjohnny.

I tried regenerating with the changes in this PR and it changes a broken implementation of https://github.com/planet-a-ventures/affinity-node/blob/32fd4f2deea3cb7723fca17353bb35d6b2d19bda/openapi/2024-08-28.json

into a working one with correct discrimination of oneOfs.

Here is the runtime output of the code generated from current master:

ListEntryWithEntityPaged {
  data: [
    ListEntryWithEntity {
      type: "company",
      id: 167627852,
      createdAt: 2024-08-30T10:37:56.000Z,
      creatorId: 54576635,
      entity: Person {
        id: 297186243,
        firstName: undefined,
        lastName: undefined,
        primaryEmailAddress: undefined,
        emailAddresses: undefined,
        type: undefined,
        fields: [ [Field], [Field], [Field], [Field], [Field] ]
      }
    }
  ],
  pagination: Pagination { prevUrl: null, nextUrl: null }
}

please note how the entity has been deserialized into a class Person, albeit the discriminator (type) clearly states it has to be of type "company".

With the changes in this PR, this becomes:

ListEntryWithEntityPaged {
  data: [
    CompanyListEntry {
      id: 167627852,
      type: "company",
      createdAt: 2024-08-30T10:37:56.000Z,
      creatorId: 54576635,
      entity: Company {
        id: 297186243,
        name: "joschas testdealflow company",
        domain: "joscha.io",
        domains: [ "joscha.io" ],
        isGlobal: false,
        fields: [ [Field], [Field], [Field], [Field], [Field] ]
      }
    }
  ],
  pagination: Pagination { prevUrl: null, nextUrl: null }
}

which is correct (see how the entity is correctly of class Company).

I also noticed that this PR does make the changes in #19481 superfluous. E.g. if this PR is merged, #19481 (4238f17) can be reverted.

@joscha
Copy link
Contributor

joscha commented Aug 30, 2024

I created #19494 that has the necessary revert, upstream changes from master and regeneration of fixtures that have recently been added since this PR was opened.

@joscha
Copy link
Contributor

joscha commented Aug 30, 2024

This pull request either needs all commits from #19494 or #19494 can be merged and it will automatically close this one as well, as it contains all the commits. I tried opening #19494 directly against your fork @ksvirkou-hubspot, however because of the commits that have made it to master in the last month since this PR was last rebased, there are dozens of commits in the diff, so I opted to instead create it against origin.

@macjohnny I can confirm that the generated code in this PR fixes the issue discussed before, both from a semantic perspective when looking at the code as well as in runtime (tried it against the live API, see above), so we should try to get this PR in as soon as possible, if we can, I think it will help anyone with swagger definitions that contain oneOfs.

@macjohnny
Copy link
Member

@ksvirkou-hubspot thanks for the fix! @joscha thanks for investigating and preparing the final PR. all merged.

@joscha
Copy link
Contributor

joscha commented Sep 6, 2024

@macjohnny I just noticed that this change seems to generate nullable enums with an extra 'null' string value.

E.g:

          "enrichmentSource": {
            "description": "The source of the data in this Field (if it is enriched)",
            "enum": [
              "affinity-data",
              "dealroom",
              null
            ],
            "example": "affinity-data",
            "type": "string",
            "nullable": true
          },

becomes

export enum FieldEnrichmentSourceEnum {
    AffinityData = 'affinity-data',
    Dealroom = 'dealroom',
    Null = 'null'
}

which is incorrect as 'null' as a string is not a valid enum value. I will try and produce a fix for this.

@macjohnny
Copy link
Member

thanks for pointing this out and fixing!

@joscha
Copy link
Contributor

joscha commented Sep 6, 2024

thanks for pointing this out and fixing!

fix is in #19540

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

Successfully merging this pull request may close these issues.

4 participants