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-fetch] correct oneOf model to or not concat when handling enums #10017

Closed
wants to merge 17 commits into from
Closed

Conversation

prince-chrismc
Copy link

@prince-chrismc prince-chrismc commented Jul 22, 2021

This was report in 2019 #4387 (comment) shortly after the original PR the author noted it was not tested... I do not know how to add test code but if you point me in the right direction I can try!

still broken #5202
resolves #6513

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/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    For Windows users, please run the script in Git BASH.
  • File the PR against the correct branch: master, 5.3.x, 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@TiFu (2017/07) @taxpon (2017/07) @sebastianhaas (2017/07) @kenisteward (2017/07) @Vrolijkx (2017/09) @macjohnny (2018/01) @topce (2018/10) @akehir (2019/07) @petejohansonxo (2019/11) @amakhrov (2020/02)

}
return {

'combinedStringEnum': !exists(json, 'combined-string-enum') ? undefined : StringEnum | StringEnumTwoFromJSON(json['combined-string-enum']),
Copy link
Author

Choose a reason for hiding this comment

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

This is still incorrectly generated

}
return {

'combined-string-enum': StringEnum | StringEnumTwoToJSON(value.combinedStringEnum),
Copy link
Author

Choose a reason for hiding this comment

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

This is still incorrectly generated

@prince-chrismc
Copy link
Author

I do not know how to split datatype when it's a combination

'{{name}}': {{^required}}!exists(json, '{{baseName}}') ? undefined : {{/required}}{{datatype}}FromJSON(json['{{baseName}}']),

if ((json === undefined) || (json === null)) {
return json;
}
return StringEnumFromJSONTyped(json, true) || StringEnumTwoFromJSONTyped(json, true);
Copy link
Author

Choose a reason for hiding this comment

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

Previously this came out as a object spread of the wrong type {...FromJSONTyped(json, true), ...FromJSONTyped(json, true) }

Copy link
Contributor

Choose a reason for hiding this comment

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

is it valid for an enum to include an empty string ("") as a value? If so, we might want to be stricter here and use ?? instead of ||

Copy link
Contributor

Choose a reason for hiding this comment

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

however, apart from an empty string, this || will never work at all. StringEnumFromJSONTyped always returns some value (because a null check is done just before it - line 36), so the second part, StringEnumTwoFromJSONTyped will never be executed

Copy link
Author

Choose a reason for hiding this comment

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

"" are valid in OAS so ?? is required ... it's also a problem for boolean and numbers so good catch.

I wrongly assumed casting json as {{classname}} would return falsy but that's not the case!

Copy link
Contributor

@amakhrov amakhrov left a comment

Choose a reason for hiding this comment

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

@prince-chrismc nice work here!

Could you please add an example of oneOf with object types, not string enums? I believe it is a more common case, and will better surface potential problems, if any

if (value === null) {
return null;
}
return StringEnumToJSON(value as StringEnum) || StringEnumTwoToJSON(value as StringEnumTwo);
Copy link
Contributor

Choose a reason for hiding this comment

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

the second part after || will never work.
It's not a big deal for string enums, which don't really convert values and only do type assertion. However, for more complex data types it's a potential problem - right?

Copy link
Author

Choose a reason for hiding this comment

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

It didn't cross my mind, our docs only use string enums. I test it out tomorrow and see!

@prince-chrismc
Copy link
Author

Could you please add an example of oneOf with object types, not string enums? I believe it is a more common case, and will better surface potential problems, if any

Can you take a look at my commit 4b40ca2 I got the following error

Root: Y:\repository\openapi-generator
[pool-1-thread-7] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\sagas-and-records)▒
[pool-1-thread-9] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\with-interfaces)▒
[pool-1-thread-2] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\default)▒
[pool-1-thread-8] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\typescript-three-plus)▒
[pool-1-thread-5] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\multiple-parameters)▒
[pool-1-thread-6] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\prefix-parameter-interfaces)▒
[pool-1-thread-3] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\enum)▒
[pool-1-thread-1] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\default-v3.0)▒
[pool-1-thread-11] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\without-runtime-checks)▒
[pool-1-thread-4] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\es6-target)▒
[pool-1-thread-10] Generating typescript-fetch (outputs to Y:\repository\openapi-generator\samples\client\petstore\typescript-fetch\builds\with-npm-version)▒
[pool-1-thread-3] Generation failed for typescript-fetch: (RuntimeException) Could not process model 'ObjectEnum'.Please make sure that your schema is correct!
java.lang.RuntimeException: Could not process model 'ObjectEnum'.Please make sure that your schema is correct!
        at org.openapitools.codegen.DefaultGenerator.generateModels(DefaultGenerator.java:499)
        at org.openapitools.codegen.DefaultGenerator.generate(DefaultGenerator.java:875)
        at org.openapitools.codegen.cmd.GenerateBatch$GenerationRunner.run(GenerateBatch.java:232)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.NullPointerException
        at org.openapitools.codegen.utils.ModelUtils.isBinarySchema(ModelUtils.java:647)
        at org.openapitools.codegen.utils.ModelUtils.isFileSchema(ModelUtils.java:659)
        at org.openapitools.codegen.languages.TypeScriptFetchClientCodegen.getTypeDeclaration(TypeScriptFetchClientCodegen.java:295)
        at org.openapitools.codegen.languages.TypeScriptFetchClientCodegen.addAdditionPropertiesToCodeGenModel(TypeScriptFetchClientCodegen.java:305)
        at org.openapitools.codegen.DefaultCodegen.fromModel(DefaultCodegen.java:2659)
        at org.openapitools.codegen.languages.TypeScriptFetchClientCodegen.fromModel(TypeScriptFetchClientCodegen.java:449)
        at org.openapitools.codegen.languages.TypeScriptFetchClientCodegen.fromModel(TypeScriptFetchClientCodegen.java:37)
        at org.openapitools.codegen.DefaultGenerator.processModels(DefaultGenerator.java:1250)
        at org.openapitools.codegen.DefaultGenerator.generateModels(DefaultGenerator.java:494)

@prince-chrismc
Copy link
Author

❤️ Thank you for the review, it's clear after 6 months I am just starting my typescript journey... 8yrs of C++ was easier 🤣

Greatly appreciate your help!

Comment on lines +4 to +9
if ((json === undefined) || (json === null)) {
return json;
}
if(!Object.values({{classname}}).includes(json as {{classname}})) {
return null;
}
Copy link
Author

Choose a reason for hiding this comment

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

Are these runtime checks allowed?

@amakhrov
Copy link
Contributor

Can you take a look at my commit 4b40ca2 I got the following error

Values of enum cannot be objects.

What I meant was trying oneOf with regular object types, not with enums at all.

@prince-chrismc
Copy link
Author

What I meant was trying oneOf with regular object types

They do not work sadly, they also fall victim to #10017 (comment)

@amakhrov
Copy link
Contributor

amakhrov commented Aug 6, 2021

hey do not work sadly, they also fall victim to #10017 (comment)

I feel it means the serialization/deserialization logic should be loosened (type-wise) - the code should try to (de-)serialize all possible properties from intersection (A & B), despite the fact the real type we work with is the union A | B.

@prince-chrismc
Copy link
Author

I am a little out of my depth, but AFAIK It does that for properties... it looks like the "type" is not well parsed... but I did not look into that section of the tool.

However your suggestion makes sense... we originally user the angular generator and that had no issue... and the templates seem much less complicated https://github.com/OpenAPITools/openapi-generator/blob/bf89f40263c6a57a76697c551d2c30c651812f7a/modules/openapi-generator/src/main/resources/typescript-angular/modelOneOf.mustache

@amakhrov
Copy link
Contributor

amakhrov commented Aug 6, 2021

The main difference between typescript-fetch and typescript-angular (apart from using a different underlying http library, of course :) ) is that typescript-fetch produces code for run-time serializing/deserializing. When doing so for nested data structures, it needs to understand at run time which model it's handling at each point, in order to call a corresponding serializer.

On the other hand, code generated by typescript-angular passes the api data through as is, so it doesn't face the same challenge with oneOf schemas.

@prince-chrismc prince-chrismc changed the title [typescript-fetch] correct oneOf model to or not concat [typescript-fetch] correct oneOf model to or not concat when handling enums Aug 6, 2021
@prince-chrismc
Copy link
Author

Is it possible to move ahead with this PR? 🙏 Sadly if the work gets anymore complicated I wouldn't be able to help fix the issue

@@ -1,10 +1,16 @@
{{>modelEnumInterfaces}}

export function {{classname}}FromJSON(json: any): {{classname}} {
export function {{classname}}FromJSON(json: any): {{classname}} | null {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is gonna be a breaking change. if an enum property is marked as required / non-nullable in the spec, all consumers expect that it's defined.
after this change some apps will likely stop compiling.

why do yo need it anyway? you can keep the original code here - return the enum value as is, without any processing

Copy link
Author

Choose a reason for hiding this comment

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

Typescript gave a me a warning about the implicit return null. I believe it can be removed

Copy link
Contributor

Choose a reason for hiding this comment

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

that's because this PR changed the code inside the function, narrowing down json type to null before returning it

Copy link
Author

@prince-chrismc prince-chrismc Aug 13, 2021

Choose a reason for hiding this comment

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

Playing around with this more... it previously returned a value that was not valid. so if you passed 7 and your enum was [0, 1, 2] you would get back 7 which is not the right type.

Copy link
Contributor

Choose a reason for hiding this comment

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

well, we trust the api response matches the spec. otherwise the code is gonna break anyway, since we don't do full shape verification

Copy link
Author

Choose a reason for hiding this comment

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

The challenge is older clients being used with newer backends which just weren't aware of the existence of such values

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say, null is as unexpected as a bogus enum value (if the property is not marked as nullable).
Again, i honestly believe it's way out of scope of the generated api client.

if (value === null) {
return null;
}
return PatternObjectToJSON(value as PatternObject) ?? PatternObjectTwoToJSON(value as PatternObjectTwo);
Copy link
Author

Choose a reason for hiding this comment

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

Finally 🎉 Got that trouble code generated!

Before it was { ...TypeOneFromJson(value), ...TypeTwoFromJson(value) } which was actually an all of? 🤔

AFAIK, With these changes we now have "its was completely parsed as Type one" or "its not TypeOne and completely parsed as TypeTwo"

Copy link
Contributor

Choose a reason for hiding this comment

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

we now have "its was completely parsed as Type one" or "its not TypeOne and completely parsed as TypeTwo"

erhm, not exactly. TypeOneFromJson only returns null if input is null, otherwise it returns an object So the second parsing (PatternObjectTwoToJSON) will never happen

@prince-chrismc
Copy link
Author

🤔 Hmm

since we don't do full shape verification

It seems like that would be required for this to work out as you point out in #10017 (comment) other wise I've made wierd code worse 🤣

There's the fact

typescript-fetch produces code for run-time serializing/deserializing #10017 (comment)

Should continue down this rabbit hole? Or is this too aggressive of a change?

@amakhrov
Copy link
Contributor

Probably the most correct way to address your case is to handle primitive values differently from objects.

perhaps need some condition here to process enums and objects differently. might need to create a flag for that in the java model, if it's not available yet

So object models will keep deserializing using an object spread ({...oneOfFirstFromJson(data), ...oneOfSecondFromJson(data)}), while enums will just pass the value through as is

@prince-chrismc
Copy link
Author

Thanks for all you help.

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

Successfully merging this pull request may close these issues.

[BUG] typescript-fetch oneOf doesn't handle discrimination by property.
2 participants