Skip to content

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

Merged
merged 1 commit into from
Mar 2, 2020

Conversation

gnuletik
Copy link
Contributor

@gnuletik gnuletik commented Jan 8, 2020

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 :)

Copy link
Collaborator

@carltongibson carltongibson left a 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 — It's a nice start.

As per the comment, I've opened #7127 to refactor to allow a straight get_components() call, parallel to (and before) get_operation(). If you wanted to rebase and adjust on top of that, it would be cool.

RESPONSE = 1
BODY = 2

def _get_item_schema(self, serializer, schema_mode, method):
Copy link
Collaborator

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...)

Copy link
Contributor Author

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.


# 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
Copy link
Collaborator

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?

Copy link
Contributor Author

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

return operation
component_schema = self._get_component_schema(path, method)

return operation, component_schema
Copy link
Collaborator

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.

Copy link
Contributor Author

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 !

@gnuletik gnuletik force-pushed the openapi-reference-components branch from 69c6355 to 83f0810 Compare January 12, 2020 08:34
Copy link
Collaborator

@carltongibson carltongibson left a 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.

@gnuletik gnuletik force-pushed the openapi-reference-components branch from f7de465 to 8e65ae9 Compare January 24, 2020 14:50
@gnuletik
Copy link
Contributor Author

Hi @carlfarrington, I rebased my branch on master.

@carlfarrington
Copy link
Contributor

@carltongibson

@gnuletik
Copy link
Contributor Author

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.
For example, if you have an ItemDetailSerializer and ItemSerializer (with different fields) who use both the same Model, you may have a component with extra fields or missing fields.

As a workaround, the schema name is deducted from the serializer.
The current solution look for a Meta.schema_component_name attribute in the serializer and use it as the schema name if present.
If not, the schema name will be serializer.__class__.__name__ minus the "Serializer" string.

Let me know what you think of it :)
Thanks!

@gnuletik
Copy link
Contributor Author

gnuletik commented Feb 5, 2020

Do you know how serializers with FileField should be handled with components ?
As stated here, the properties in the request and response may be different.

It seems that request properties should have format: binary and responses should omit format.
Does it mean that we should create one component for the Request and one for the Response?

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.

@carltongibson
Copy link
Collaborator

...using the model name as the component name could lead to a wrong schema.

Yes. I think in general it's a bit more complex than you have here. (I'm thinking on it but...)

@gnuletik
Copy link
Contributor Author

...using the model name as the component name could lead to a wrong schema.

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 ?
The serializer's name is now used instead of the model's name.

@carltongibson
Copy link
Collaborator

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?

@gnuletik
Copy link
Contributor Author

Hi @carltongibson,
Thanks for the feedback!

I think we need to allow for request and response components. Take just read-only and write-only fields — they lead to different components.

One point of component is to keep the same component for Request / Response.
For read-only and write-only fields, those are handled with the writeOnly and readOnly field of the spec : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-20

But folks also want to be able to use different serializers for different methods and such.

There is no regression in this PR that would change how the view's serializer is retrieved.
Someone is free to use different serializers for different methods.

The default implementation just needs to handle core cases, but the API needs to be correct so that folks can customize easily.

Totally agree with you! That's the point of this PR : Handle core cases and allow folks to customize it easily.

@carltongibson
Copy link
Collaborator

One point of component is to keep the same component for Request / Response.
For read-only and write-only fields, those are handled with the writeOnly and readOnly field of the spec : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-20

Yes. OK.I need to have a closer look at the details but I think this isn't far off then. Super.

@carltongibson carltongibson force-pushed the openapi-reference-components branch from f4d826d to 568452c Compare February 12, 2020 18:58
@carltongibson carltongibson changed the title Add schema references for OpenAPI generation, see #6984 Generate components for OpenAPI schemas. Feb 12, 2020
Copy link
Collaborator

@carltongibson carltongibson left a 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.

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)
Copy link
Collaborator

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.

component = view.schema.get_components(path, method)

if component is not None:
components_schemas.update(component)
Copy link
Collaborator

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.

@@ -101,6 +112,34 @@ def get_operation(self, path, method):

return operation

def _get_serializer_component_name(self, serializer):
Copy link
Collaborator

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().

@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Drop this.

return None

if hasattr(serializer.Meta, 'schema_component_name'):
return serializer.Meta.schema_component_name
Copy link
Collaborator

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.

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)
Copy link
Collaborator

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.

serializer = self._get_serializer(path, method)

if not isinstance(serializer, serializers.Serializer):
return None
Copy link
Collaborator

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. {}

component_name = self._get_serializer_component_name(serializer)

if component_name is None:
return None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Drop this.

@@ -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)}
Copy link
Collaborator

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.

# 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
Copy link
Collaborator

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.

@gnuletik
Copy link
Contributor Author

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 docs/api-guide/schemas.md).
I was thinking about:

  • Adding an introduction about components
  • Links to the OpenAPI Specs
  • Component name customization with AutoSchema(component_name="Ulysses")
  • Example of overriding get_component_name and get_components

What do you think about it ? Should I make a separate PR ?

@carltongibson
Copy link
Collaborator

Hi @gnuletik.

Should I make a separate PR ?

No, add the docs changes here. (They're integral.)

Thanks for the effort! 🥇

@gnuletik gnuletik force-pushed the openapi-reference-components branch 2 times, most recently from d295b9f to d6fbc32 Compare February 28, 2020 18:03
Copy link
Collaborator

@carltongibson carltongibson left a 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! 🥇

=======
### 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.
Copy link
Collaborator

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...


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.
Copy link
Collaborator

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")

@gnuletik gnuletik force-pushed the openapi-reference-components branch from d6fbc32 to 7c8edbc Compare March 2, 2020 17:37
@carltongibson carltongibson merged commit 8aa8be7 into encode:master Mar 2, 2020
@martyzz1
Copy link

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?
Why base the component/schema on the underlying Model - rather than the Serializer? (which was the route I was going to take as I thought it might be a more comprehensive solution)

@gnuletik
Copy link
Contributor Author

Thanks @martyzz1 !
At first, we used the Model name as a base for the component/schema.
Then, we changed it and we are now using the Serializer's name :)

See the get_component_name method:
https://github.com/encode/django-rest-framework/pull/7124/files#diff-16b3da8ab434dab83a9fcf30f1322643R161

@martyzz1
Copy link

Thanks @gnuletik.

pchiquet pushed a commit to pchiquet/django-rest-framework that referenced this pull request Nov 17, 2020
sigvef pushed a commit to sigvef/django-rest-framework that referenced this pull request Dec 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OpenAPI 3.0 should generate and reference #/components/schemas/
4 participants