-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Generate components for OpenAPI schemas. #7124
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
Generate components for OpenAPI schemas. #7124
Conversation
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.
rest_framework/schemas/openapi.py
Outdated
RESPONSE = 1 | ||
BODY = 2 | ||
|
||
def _get_item_schema(self, serializer, schema_mode, method): |
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.
I don't see the benefit of this refactoring. It's less clear than the separate methods, and doesn't save a lot of duplication (what with the enum and the bit if...else
on the schema_mode...)
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.
You're right! I removed this refactoring.
rest_framework/schemas/openapi.py
Outdated
|
||
# If the model has no model, then the serializer will be inlined | ||
if not hasattr(serializer, 'Meta') or not hasattr(serializer.Meta, 'model'): | ||
return 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.
Q: Do we need to allow for different components (with different fields) for requests and responses, at least?
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.
Good question ! We could have have two components like ModelNameRequest
and ModelNameResponse
.
However, we may better keep one component and use the readOnly
and writeOnly
properties.
This is the generator's job to deduce that :
- properties having
writeOnly: true
should not be in a Response - properties having
readOnly: true
should not be in Request
More information here : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md
rest_framework/schemas/openapi.py
Outdated
return operation | ||
component_schema = self._get_component_schema(path, method) | ||
|
||
return operation, component_schema |
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.
I see why you've done this, but I think it would be better to refactor to avoid the tuple return here. c.f. #7127.
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.
Thanks ! I rebased my PR on your's !
69c6355
to
83f0810
Compare
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.
Hi @gnuletik. Thanks for this. If you can now rebase on master
again I'll give it a proper look.
f7de465
to
8e65ae9
Compare
Hi @carlfarrington, I rebased my branch on |
Hey there, After using this fork I realized an issue with the PR : using the model name as the component name could lead to a wrong schema. As a workaround, the schema name is deducted from the serializer. Let me know what you think of it :) |
Do you know how It seems that request properties should have There is an example in the OpenAPI 3.0.2 spec : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#considerations-for-file-uploads but the schemas is inlined. I can't find any example with a component referenced. |
Yes. I think in general it's a bit more complex than you have here. (I'm thinking on it but...) |
What's wrong with the actual implementation ? |
Hi @gnuletik. I think we need to allow for request and response components. Take just read-only and write-only fields — they lead to different components. But folks also want to be able to use different serializers for different methods and such. The default implementation just needs to handle core cases, but the API needs to be correct so that folks can customize easily. Make sense? |
Hi @carltongibson,
One point of component is to keep the same component for Request / Response.
There is no regression in this PR that would change how the view's serializer is retrieved.
Totally agree with you! That's the point of this PR : Handle core cases and allow folks to customize it easily. |
Yes. OK.I need to have a closer look at the details but I think this isn't far off then. Super. |
f4d826d
to
568452c
Compare
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.
Right. 🙂 This is good.
But I'm not seeing why we don't always just use the serialiser to create a component, regardless of whether there's a model or not. (I don't have to be using ModelSerializer
.)
See the inline comments. They should make sense.
The existing tests will need adjusting, and we should document the get_components()
and get_component_name()
methods, plus the __init__()
arg component_name
.
But I don't think we need to do anything more complicated that that.
rest_framework/schemas/openapi.py
Outdated
paths = {} | ||
_, view_endpoints = self._get_paths_and_endpoints(None if public else request) | ||
for path, method, view in view_endpoints: | ||
if not self.has_view_permissions(path, method, view): | ||
continue | ||
|
||
operation = view.schema.get_operation(path, method) | ||
component = view.schema.get_components(path, method) |
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.
name this components
with an s
just in case folks want to return multiple components.
rest_framework/schemas/openapi.py
Outdated
component = view.schema.get_components(path, method) | ||
|
||
if component is not None: | ||
components_schemas.update(component) |
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.
Drop the guard. Let's have get_components always return a dict.
rest_framework/schemas/openapi.py
Outdated
@@ -101,6 +112,34 @@ def get_operation(self, path, method): | |||
|
|||
return operation | |||
|
|||
def _get_serializer_component_name(self, serializer): |
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.
Rename this just get_component_name()
.
rest_framework/schemas/openapi.py
Outdated
@@ -101,6 +112,34 @@ def get_operation(self, path, method): | |||
|
|||
return operation | |||
|
|||
def _get_serializer_component_name(self, serializer): | |||
if not hasattr(serializer, 'Meta'): | |||
return 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.
Drop this.
rest_framework/schemas/openapi.py
Outdated
return None | ||
|
||
if hasattr(serializer.Meta, 'schema_component_name'): | ||
return serializer.Meta.schema_component_name |
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.
Drop this. Instead have AutoSchema take an __init__()
kwarg, component_name
. If that's passed use it. Otherwise generate a name as per below.
Usage would be:
class MyView(APIView):
schema = AutoSchema(component_name="Ulysses")
If folks need more complex usage, they can override the method.
rest_framework/schemas/openapi.py
Outdated
component_name = serializer.__class__.__name__ | ||
# We remove the "serializer" string from the class name. | ||
pattern = re.compile("serializer", re.IGNORECASE) | ||
return pattern.sub("", component_name) |
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.
These three lines are probably fine for a default implementation.
rest_framework/schemas/openapi.py
Outdated
serializer = self._get_serializer(path, method) | ||
|
||
if not isinstance(serializer, serializers.Serializer): | ||
return 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.
Let's always return a dict. {}
rest_framework/schemas/openapi.py
Outdated
component_name = self._get_serializer_component_name(serializer) | ||
|
||
if component_name is None: | ||
return 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.
Drop this.
rest_framework/schemas/openapi.py
Outdated
@@ -491,6 +530,10 @@ def _get_serializer(self, path, method): | |||
.format(view.__class__.__name__, method, path)) | |||
return None | |||
|
|||
def _get_reference(self, serializer): | |||
component_name = self._get_serializer_component_name(serializer) | |||
return {'$ref': '#/components/schemas/{}'.format(component_name)} |
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.
I think inline this function.
rest_framework/schemas/openapi.py
Outdated
# If possible, the serializer should use a reference | ||
item_schema = self._get_reference(serializer) | ||
else: | ||
# There is no model, we'll map the serializer's fields |
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.
Then we can drop this block.
Thanks for your review and your time @carltongibson ! I updated the PR. I'll also be glad to write some user-documentation about it (inside
What do you think about it ? Should I make a separate PR ? |
Hi @gnuletik.
No, add the docs changes here. (They're integral.) Thanks for the effort! 🥇 |
d295b9f
to
d6fbc32
Compare
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.
Hi @gnuletik.
Thank you for this. It is really nice. Just a couple of comments, a rebase needed.
Good work! 🥇
docs/api-guide/schemas.md
Outdated
======= | ||
### Components | ||
|
||
Since DRF 3.12, Schema uses the [OpenAPI Components](openapi-components). This method define components in the schema and [referenced them](openapi-reference) inside request and response objects. The component's name is deduced from the Serializer's name. |
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.
defines and references.
By default, the component's name...
docs/api-guide/schemas.md
Outdated
|
||
Using OpenAPI's components provides the following advantages: | ||
* The schema is more readable and lightweight. | ||
* If you use the schema to generate a SDK (using [openapi-generator](openapi-generator) or [swagger-codegen](swagger-codegen)). The generator can name your SDK's models. |
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.
an SDK
("es-dee-kay")
d6fbc32
to
7c8edbc
Compare
I came looking to see if anybody else was looking into this as I was thinking of doing it myself - if nobody had done it already... So great work!! I have a question, and figured this might be a better place to ask it? |
Thanks @martyzz1 ! See the |
Thanks @gnuletik. |
Closes #6984
As described in this issue, the OpenAPI specs generated by DRF may use a reference to existing models.
This PR add a components/schema property in the OpenAPI specs for serializers having a
model
property. The schema's name is the model's class name.For serializers without model, the properties are inlined as before.
I added a sample test but I may add other ones if this PR is okay for you.
Edit: The schema references are also discussed here.
Thanks :)