Skip to content

too strict criteria for merging properties with allOf #1090

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

Closed
eli-bl opened this issue Aug 6, 2024 · 0 comments · Fixed by #1096
Closed

too strict criteria for merging properties with allOf #1090

eli-bl opened this issue Aug 6, 2024 · 0 comments · Fixed by #1096

Comments

@eli-bl
Copy link
Collaborator

eli-bl commented Aug 6, 2024

(I think that this overlaps with #1083 - feel free to close one or the other as a duplicate - I think there are multiple problems with allOf, and this is a specific one)

Describe the bug
Conditions:

  • Schema B uses allOf to derive properties from Schema A.
  • Schema B also overrides one of those properties with its own definition.
  • The property is different in at least one way not including its type, enum values, or required status.

Expected behavior:

  • The client generator merges the two definitions, and implements validation for the property based on the criteria from both schemas.

Observed behavior:

  • The client generator fails to parse Schema B. The underlying error is "Properties has conflicting values".

OpenAPI Spec File
https://gist.github.com/eli-bl/3a48142d312578f526992a175c3d1ce4

In this example, the Cat schema is meant to be identical to Animal except that the id property must match a specific regex. This is a valid spec according to https://editor.swagger.io, and the OpenAPI 3 spec does not suggest any reason for it to be invalid.

Desktop (please complete the following information):

  • OS: macOS 14.5
  • Python Version: 3.8.15
  • openapi-python-client version 0.21.2

Additional context
As expected, the generator does allow Schema B to further restrict the enum values for the property. However, it does not allow any other changes under type for a property. For properties that are objects, this is especially problematic, since it means that you can't redeclare a property to use an object type that extends its original type with more properties. For instance, suppose Animal has a property colors of type AnimalPartColors, which in turn has a single property eyes (example: "colors": {"eyes": "blue"}). And suppose you define a type CatPartColors that derives from AnimalPartColors but also adds fur ("colors": {"eyes": "blue", "fur": "gray"}). The generator correctly allows you to do that— except that will fail if you then try to redefine colors in Cat to have type CatPartColors. I believe that this behavior is contrary to the intention of the OpenAPI spec. Using allOf to extend an object type with extra properties is a standard pattern, and the expected behavior in this case would be to say that the validation rules for colors in a Cat are the ones defined in CatPartColors.

Any other change to the property also triggers the error even if it would not change the validation behavior of the property. For instance, changing the description (such as, in this example, calling it "unique identifier of the cat" instead of "unique identifier of the animal") is not allowed.

The relevant logic is here: https://github.com/openapi-generators/openapi-python-client/blob/v0.21.2/openapi_python_client/parser/properties/model_property.py#L250

The OpenAPI spec does not clarify what should happen to non-validation-related attributes such as description in this scenario, where it isn't really possible to define the intersection between one definition and the other. An arguably intuitive behavior would be to use the description that is defined latest (in this example, "unique identifier of the cat")-- which is what the behavior of https://editor.swagger.io appears to be.

github-merge-queue bot pushed a commit that referenced this issue Sep 7, 2024
Fixes #1090 (with some limitations - see below)

This PR allows the generator to work with some valid schemas where the
same property name is specified by more than one schema within an
`allOf`, where previously it would have failed.

# Background

As described in the issue, OpenAPI and JSON Schema define `allOf` in a
fairly vague and tolerant way. If a schema is defined like this:
```yaml
  C:
    allOf:
      - $ref: "/path/to/schema/A" 
      - $ref: "/path/to/schema/B"
```

—then the validation for C is "must pass validation for A, and for B". A
and B are both objects, and they both have property X, then the value
for X must pass validation for A.X and B.X; the validation is the
strictest subset of both.

This does not map very cleanly to the client generator's internal model.
Because its ultimate goal is to producing Python model classes with
properties of specific types, we need to be able to "merge" A.X and B.X
to produce a single instance of some ___Property class, whose attributes
are derived from both.

The logic for doing this was narrower than necessary. If, for instance,
A.X and B.X only differed in terms of their `description`, or if one of
them specified a `default` value and the other did not, the generator
would reject them as being too different.

# Solution

This PR defines a more nuanced property merge logic, as follows:

- If both versions of the property are of the same type, the result is a
property of the same type. Its validation rules, if any, are the
strictest subset of the validation rules for both (for instance, "string
with max length 3" and "string with max length 5" produces "string with
max length 3").
- Int and number (aka float) properties can always be merged; the result
is an int property (since "must be integer" is a stricter rule than
"must be a number"). This is only invalid if there is a non-integer
default value.
- "Any" can be merged with any property type; the result is that
property type (since "must be a ___" is stricter than "can be
anything").
- For common string-valued attributes such as `description`, earlier
values are overridden by later values, on the assumption that the second
schema is meant to be an extension or specialization of the first.

It also preserves helpful aspects of the original implementation:

- Two enums can be merged, if the values for one are a subset of the
other; the result uses the subset.
- A string enum can be merged with a string, or an int enum with an int;
the result is an enum.

# Limitations

Due to limitations of our internal model, some combinations of
properties that are valid in OpenAPI will still be rejected by this
implementation:

- Two string properties with different `pattern` attributes cannot be
merged, even if there are values that could match both of those regexes.
- Two list types that contain different schemas cannot be merged, even
though logically this should mean "every element in the list must match
both of those schemas".
- OpenAPI allows `minimum` and `maximum` to be specified for numeric
types, but (as far as I can tell) the generator currently ignores these
altogether, so the merge logic doesn't take them into account either.

# Compatibility

This PR preserves the previous schema parsing behavior in all cases
where the generator would have previously succeeded. The new behaviors
all apply to valid schemas where the generator would have previously
failed.

---------

Co-authored-by: Dylan Anthony <[email protected]>
Co-authored-by: Dylan Anthony <[email protected]>
micha91 pushed a commit to micha91/openapi-python-client that referenced this issue May 13, 2025
…1096)

Fixes openapi-generators#1090 (with some limitations - see below)

This PR allows the generator to work with some valid schemas where the
same property name is specified by more than one schema within an
`allOf`, where previously it would have failed.

# Background

As described in the issue, OpenAPI and JSON Schema define `allOf` in a
fairly vague and tolerant way. If a schema is defined like this:
```yaml
  C:
    allOf:
      - $ref: "/path/to/schema/A" 
      - $ref: "/path/to/schema/B"
```

—then the validation for C is "must pass validation for A, and for B". A
and B are both objects, and they both have property X, then the value
for X must pass validation for A.X and B.X; the validation is the
strictest subset of both.

This does not map very cleanly to the client generator's internal model.
Because its ultimate goal is to producing Python model classes with
properties of specific types, we need to be able to "merge" A.X and B.X
to produce a single instance of some ___Property class, whose attributes
are derived from both.

The logic for doing this was narrower than necessary. If, for instance,
A.X and B.X only differed in terms of their `description`, or if one of
them specified a `default` value and the other did not, the generator
would reject them as being too different.

# Solution

This PR defines a more nuanced property merge logic, as follows:

- If both versions of the property are of the same type, the result is a
property of the same type. Its validation rules, if any, are the
strictest subset of the validation rules for both (for instance, "string
with max length 3" and "string with max length 5" produces "string with
max length 3").
- Int and number (aka float) properties can always be merged; the result
is an int property (since "must be integer" is a stricter rule than
"must be a number"). This is only invalid if there is a non-integer
default value.
- "Any" can be merged with any property type; the result is that
property type (since "must be a ___" is stricter than "can be
anything").
- For common string-valued attributes such as `description`, earlier
values are overridden by later values, on the assumption that the second
schema is meant to be an extension or specialization of the first.

It also preserves helpful aspects of the original implementation:

- Two enums can be merged, if the values for one are a subset of the
other; the result uses the subset.
- A string enum can be merged with a string, or an int enum with an int;
the result is an enum.

# Limitations

Due to limitations of our internal model, some combinations of
properties that are valid in OpenAPI will still be rejected by this
implementation:

- Two string properties with different `pattern` attributes cannot be
merged, even if there are values that could match both of those regexes.
- Two list types that contain different schemas cannot be merged, even
though logically this should mean "every element in the list must match
both of those schemas".
- OpenAPI allows `minimum` and `maximum` to be specified for numeric
types, but (as far as I can tell) the generator currently ignores these
altogether, so the merge logic doesn't take them into account either.

# Compatibility

This PR preserves the previous schema parsing behavior in all cases
where the generator would have previously succeeded. The new behaviors
all apply to valid schemas where the generator would have previously
failed.

---------

Co-authored-by: Dylan Anthony <[email protected]>
Co-authored-by: Dylan Anthony <[email protected]>
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 a pull request may close this issue.

1 participant