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

Enums as separate models #1432

Merged
merged 11 commits into from May 23, 2020
Merged

Enums as separate models #1432

merged 11 commits into from May 23, 2020

Conversation

calvinwyoung
Copy link
Contributor

Change Summary

This PR updates the OpenAPI schema generation behavior so that all enums are outputted as separate models, following the recipe for reusable enums. The primary benefit is to improve support for OpenAPI code generation tools — more on this below.

To illustrate the changes, suppose we have the following models:

class Foo(str, enum.Enum):
    A = 'A'
    B = 'B'

class ModelA(pydantic.BaseModel):
    foo: Foo    

class ModelB(pydantic.BaseModel):
    foo: Foo

Prior to this PR, the generated schema would look like the following:

{
    "definitions": {
        "ModelA": {
            "title": "ModelA",
            "type": "object",
            "properties": {
                "foo": {
                    "title": "Foo",
                    "enum": [
                        "A",
                        "B"
                    ],
                    "type": "string"
                }
            },
            "required": [
                "foo"
            ]
        },
        "ModelB": {
            "title": "ModelB",
            "type": "object",
            "properties": {
                "foo": {
                    "title": "Foo",
                    "enum": [
                        "A",
                        "B"
                    ],
                    "type": "string"
                }
            },
            "required": [
                "foo"
            ]
        }
    }
}

Notice how the enum values are duplicated in both the Foo and Bar definitions. As a result, an OpenAPI code generation tool will create 2 definitions of the enum — one for ModelAFoo and one for ModelBFoo. This is obviously not ideal as it makes it harder to reuse the enum definition in the client code.

With this PR, the generated schema will instead look like this:

{
    "definitions": {
        "Foo": {
            "title": "Foo",
            "enum": [
                "A",
                "B"
            ],
            "type": "string"
        },
        "ModelA": {
            "title": "ModelA",
            "type": "object",
            "properties": {
                "foo": {
                    "$ref": "#/definitions/Foo"
                }
            },
            "required": [
                "foo"
            ]
        },
        "ModelB": {
            "title": "ModelB",
            "type": "object",
            "properties": {
                "foo": {
                    "$ref": "#/definitions/Foo"
                }
            },
            "required": [
                "foo"
            ]
        }
    }
}

As a result, an OpenAPI code generation tool will output a single Foo enum definition that's referenced by both ModelA and ModelB.

This PR works seamlessly with fastapi v0.54.1 and with openapi-generator-cli via the openapitools/openapi-generator-cli docker image v4.3.0.

Related issue number

#1173

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • changes/<pull request or issue id>-<github username>.md file added describing change
    (see changes/README.md for details)

@codecov
Copy link

codecov bot commented Apr 25, 2020

Codecov Report

Merging #1432 into master will not change coverage.
The diff coverage is 100.00%.

@@            Coverage Diff            @@
##            master     #1432   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         3747      3793   +46     
  Branches       742       752   +10     
=========================================
+ Hits          3747      3793   +46     
Impacted Files Coverage Δ
pydantic/schema.py 100.00% <100.00%> (ø)
pydantic/types.py 100.00% <0.00%> (ø)
pydantic/utils.py 100.00% <0.00%> (ø)
pydantic/fields.py 100.00% <0.00%> (ø)
pydantic/typing.py 100.00% <0.00%> (ø)
pydantic/networks.py 100.00% <0.00%> (ø)
pydantic/validators.py 100.00% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f96c4dc...7aed1d5. Read the comment docs.

@calvinwyoung
Copy link
Contributor Author

@samuelcolvin One of the fastapi tests is currently failing since it's expecting an OpenAPI schema for the old enum behavior:
https://github.com/tiangolo/fastapi/blob/master/tests/test_tutorial/test_path_params/test_tutorial005.py#L37

What's the best way to handle this? Should I open another PR updating the fastapi test now, or should I do it once this PR gets merged in?

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

generally looks good, but I wonder whether this needs to wait until v2.

@tiangolo do you have an opinion on whether this would be considered a breaking change?

@@ -516,6 +516,27 @@ def model_type_schema(
return out_schema, definitions, nested_models


def enum_process_schema(enum: EnumMeta) -> Dict[str, Any]:
"""
Take a single ``enum`` and generate its schema.
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
Take a single ``enum`` and generate its schema.
Take a single `enum` and generate its schema.

docstrings currently aren't rendered as rst or md, but if they ever are, it'll be as markdown, see #1339.

And below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds awesome. Would it be helpful if I replaced all of the double-tick characters with ` in this file? Or would you prefer that I only update the double-ticks characters that were added by this PR to keep it cleaner?

pydantic/schema.py Outdated Show resolved Hide resolved
pydantic/schema.py Outdated Show resolved Hide resolved
pydantic/schema.py Outdated Show resolved Hide resolved
pydantic/schema.py Outdated Show resolved Hide resolved
@samuelcolvin
Copy link
Member

@samuelcolvin One of the fastapi tests is currently failing since it's expecting an OpenAPI schema for the old enum behavior:
https://github.com/tiangolo/fastapi/blob/master/tests/test_tutorial/test_path_params/test_tutorial005.py#L37

What's the best way to handle this? Should I open another PR updating the fastapi test now, or should I do it once this PR gets merged in?

Should be fixed if you rebase.

@calvinwyoung
Copy link
Contributor Author

@samuelcolvin One of the fastapi tests is currently failing since it's expecting an OpenAPI schema for the old enum behavior:
https://github.com/tiangolo/fastapi/blob/master/tests/test_tutorial/test_path_params/test_tutorial005.py#L37
What's the best way to handle this? Should I open another PR updating the fastapi test now, or should I do it once this PR gets merged in?

Should be fixed if you rebase.

@samuelcolvin Got it. Also thanks for the comments — I just pushed the suggested fixes.

I'll defer to you and @tiangolo with regard to the release schedule. In theory this shouldn't be a breaking change so long as the end user is following the Open API spec. But I also understand these things don't always work out so cleanly.

@samuelcolvin
Copy link
Member

@calvinwyoung can you rebase to master.

@tiangolo would love your feedback on the V2 decision.

@tiangolo
Copy link
Member

Sorry for the delay on this... I think I finally found a way to filter and label GitHub notifications with mentions 😅

I like this! 🚀 🎉

I've been trying to think of a way that this could break something badly enough as to be considered a "breaking change", but I haven't found anything. I don't expect FastAPI users to test their OpenAPI schemas as FastAPI already tests the generation of valid OpenAPI.

The only concern I have is, @calvinwyoung , have you tested how this looks like in the /docs Swagger UI?

But apart from that I think this should be safe to release in a MINOR bump.

I can update the tests in FastAPI to accept the schemas generated by this and release a minor version, that should make tests pass here. ✔️

@samuelcolvin
Copy link
Member

Oh so the failing FastAPI tests are real - catching an actual change with a test! 😆

I guess you'll have to get the test to pass with both the old and new schema for a while. Let us know when you've updated FastAPI and we'll get tests passing here.

@tiangolo
Copy link
Member

tiangolo commented May 23, 2020

So, I rushed to add the extra check in the tests and release a new version. But now I realized that it breaks Swagger UI 🤦 .

I'm now checking what needs to be changed in this PR and in FastAPI, I'm getting deeper and deeper into this... 😅

I'll keep reporting here what I find.

Would it be fine if I commit on top of this same branch? Or would it be better if I submit a new PR with extra changes on top of this?

@calvinwyoung
Copy link
Contributor Author

@tiangolo Thanks so much for taking a look at this. I had thought I tested the Swagger UI previously, and it seemed to work okay. Before this PR, the Schemas section rendered something like this:

Screen Shot 2020-05-23 at 9 12 18 AM

After this PR, the schemas render like this:
Screen Shot 2020-05-23 at 9 26 32 AM

And we also have a new Foo model:
Screen Shot 2020-05-23 at 9 26 44 AM

Where else are you seeing breaking changes? And yes, feel free to commit directly on top of the same branch! Thanks again — really appreciate the work you and @samuelcolvin have put into maintaining these two incredible projects 🥇

as they now have independent schemas, they kinda behave like top-level models, and should be taken into account for top level definitions
@samuelcolvin
Copy link
Member

Would it be fine if I commit on top of this same branch? Or would it be better if I submit a new PR with extra changes on top of this?

Fine by me if you commit directly to the branch for this PR, it might be simpler than a PR into this PR. I hope @calvinwyoung doesn't mind.

@tiangolo
Copy link
Member

I think that should work 🚀

I updated FastAPI, it ended up touching a bunch of places 😅 The main issue was that enums are supported for parameters in path, cookie, query, etc. And those didn't have a way to collect additional schema definitions. But this is supported now in 0.55.1 (just released).

I updated it here to include enums when collecting flat models and treat them as sort of top-level models for schema generation, as they get their own independent definitions. In short, this works correctly with the current FastAPI (and any other libraries that use this).

I added a couple of alias types to reduce duplication, but that can be changed if you prefer @samuelcolvin .

If you think this is fine, it's also ready from my side 🙂 🚀

@samuelcolvin samuelcolvin merged commit 5195e55 into pydantic:master May 23, 2020
@samuelcolvin
Copy link
Member

Awesome, thank you both.

@calvinwyoung calvinwyoung deleted the enums-as-separate-models branch May 23, 2020 18:14
@calvinwyoung
Copy link
Contributor Author

@tiangolo Ah yes that makes complete sense. Thank you!

PrettyWood added a commit to ToucanToco/toucan-connectors that referenced this pull request Jul 27, 2020
PrettyWood added a commit to ToucanToco/toucan-connectors that referenced this pull request Jul 27, 2020
PrettyWood added a commit to ToucanToco/toucan-connectors that referenced this pull request Jul 27, 2020
PrettyWood added a commit to ToucanToco/toucan-connectors that referenced this pull request Jul 27, 2020
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.

None yet

3 participants