-
-
Notifications
You must be signed in to change notification settings - Fork 209
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
add discriminator property support #1154
base: main
Are you sure you want to change the base?
Conversation
a9c27ab
to
964381a
Compare
c2f78b6
to
de524e9
Compare
@@ -2841,15 +2841,17 @@ | |||
"modelType": { | |||
"type": "string" | |||
} | |||
} | |||
}, | |||
"required": ["modelType"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is necessary in order for the discriminator in the spec to really be valid: a discriminator property must be a required property in all of the variant schemas. My current implementation wouldn't actually catch a mistake like this, but I figured it was best to have the test spec be valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least, that's my reading of what OpenAPI says. I'm basing it on the statement "The expectation now is that a property with name petType MUST be present in the response payload, and the value will correspond to the name of a schema defined in the OAS document."
However, this is a case (one of many) of the OpenAPI specification using a mix of normative and non-normative language. The MUST seems unambiguous, but it's in a paragraph that's describing this specific example with types of pets, where the schemas do say the property is required. So do they mean a discriminator property must always be required, or just that that's the behavior this example is illustrating?
Logically I feel like it makes sense for it to be required, and if it isn't, then I'm not sure what the client behavior should be: (a) refuse to parse the object if the property is missing, or (b) just fall back to "try parsing it as each of these types until one of them works"? What I've implemented so far in the generated code is (a).
@@ -67,16 +75,7 @@ def build( | |||
return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas | |||
sub_properties.append(sub_prop) | |||
|
|||
def flatten_union_properties(sub_properties: list[PropertyProtocol]) -> list[PropertyProtocol]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broke this out into a separate helper below, since it doesn't rely on any closure variables.
de524e9
to
caa81a7
Compare
2136c14
to
1956f68
Compare
1956f68
to
0496ff8
Compare
Here's a case where I'm not 100% sure what the behavior should be: components:
schemas:
ModelA:
type: object
properties:
modelType:
type: string
ModelB:
type: object
properties:
modelType:
type: string
NullType:
type: null
NullableUnionType:
oneOf:
- $ref: "#/components/schemas/ModelA"
- $ref: "#/components/schemas/ModelB"
- $ref: "#/components/schemas/NullType"
discriminator:
property: modelType Should this be considered valid? In my current implementation, it is not valid, because that's my literal reading of this statement from the spec: "The expectation now is that a property with name [the discriminator property name] MUST be present in the response payload." If the value is null, then it does not have a However, like many things in OpenAPI, the spec language doesn't really say "if you do this, it's a malformed spec"; it's just a thing that would not have a well-defined behavior if you were writing a validator or a client generator. When someone raised a question about this scenario in the OpenAPI spec repo, the official answer was basically that. They just advised people that instead of writing their specs that way, to avoid ambiguity they should do it like this: NullableUnionType:
oneOf:
- $ref: "#/components/schemas/NullType"
- oneOf:
- $ref: "#/components/schemas/ModelA"
- $ref: "#/components/schemas/ModelB"
discriminator:
property: modelType My implementation does support that nested approach. What I'm wondering is whether it would make sense to loosen it so it also supports the questionably-valid shorter version, i.e. have a specific loophole for letting one of the variants be null, simply because I'm guessing it's not uncommon for people to write specs that way. And if so, should it also allow an inline |
Putting this back to draft because I have a couple ideas to simplify/clarify things. |
9e71936
to
2941451
Compare
@@ -32,6 +32,7 @@ class ModelProperty(PropertyProtocol): | |||
relative_imports: set[str] | None | |||
lazy_imports: set[str] | None | |||
additional_properties: Property | None | |||
ref_path: ReferencePath | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I pushed some more changes to simplify things a bit. There was some extra validation that I could add in a follow-up PR, which I don't think is essential to the feature. I was also thinking about adding a new category of end-to-end tests that verify the behavior of the generated code (importing a generated model class and calling |
{% set _discriminator_properties = [] -%} | ||
{% for discriminator in property.discriminators %} | ||
{{- _discriminator_properties.append(discriminator.property_name) or "" -}} | ||
if not isinstance(data, dict): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As described in comments in union.py
, there can be multiple discriminators if there was a nested union. The logic here tries each discriminator property in order; if that property exists, it checks the value, and if it finds a matching class, it uses that class. If no matches were found for any discriminator then it raises an error.
344ec97
to
de1ddf3
Compare
This more fully fixes #219, and supersedes #717.
Any valid discriminator definition as described in OpenAPI 3.1.0 should work correctly with these changes, including:
mapping
that specifies all valid discriminator values.mapping
, in which the discriminator value for each type is the simple name of the type (equivalent to{"Model1": {"$ref": "#/components/schemas/Model1"}}
).mapping
that specifies values for some classes, but not all of them; in this case the default for the unspecified classes is the same as if there was nomapping
.Validation during parsing is not as thorough as it could be, since currently the property lists inside ModelProperty are not available at the time the UnionProperty is parsed (I do have some further changes in mind to improve this, but I wanted to keep the scope of this PR manageable). But there is some basic validation, like "you can't have a non-object schema, or an inline schema, if there's a discriminator"... so this is a breaking change in the sense that generation will now fail on some invalid specs that previously would have passed.