-
Notifications
You must be signed in to change notification settings - Fork 437
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 support for different read/write schemas #70
Comments
Hello. The swagger model<=> DRF serializer mapping is one-to-one. If this is not enough for your needs you will need to write separate "read" and "write" serializers. |
I understand the relationship between swagger 2 and rest serializers isn't 1 to 1; but i'm suggesting this might not be a limiting factor. For example, we know from the view if we are reading vs. writing (i.e. GET or POST). This information could be used to generate multiple swagger definitions for a single serialzier that make use of different interpretations of fields. Take the following serializer and model: class Todo(models.Model):
title = models.TextField(null=False)
detail = models.TextField(null=True)
owner = models.ForeignKey(User)
created_at = models.DateTimeField(auto_now_add=True)
class TodoSerialzier(models.Model):
owner = serializers.PrimaryKeyRelatedField(default=serializers.CurrentUserDefault(), read_only=True)
created_at = serializers.DateTimeField(read_only=True)
class Meta:
model = Todo
fields = '__all__' If I were to create a swagger spec for a paths:
/todos/:
get:
tags:
- Todos
summary: list Todos
operationId: listTodos
responses:
200:
description: a list of Todos
schema:
type: array
items:
$ref: '#/definitions/Todo'
post:
tags:
- Todos
summary: create a new Todo
operationId: createTodo
parameters:
- name: Todo
in: body
description: the initial values for the Todo model
schema:
$ref: '#/definitions/postBodyDefinition'
responses:
201:
description: the new Todo
schema:
$ref: '#/definitions/CreateTodo'
definitions:
Todo:
type: object
required:
- id
- name
- created_at
- owner
properties:
id:
type: number
name:
type: string
detail:
type: string
created_at:
type: string
owner:
type: number
CreateTodo:
type: object
required:
- name
- detail
properties:
name:
type: string
detail:
type: string In reality, there is only 1 model and 1 serializer but the spec can include 2 different definitions. These definitions are generated by considering if the current view path is GET or POST (read or write) and then using different rules when creating the schema from the serializer. I.e. if the method is GET, then all Hopefully this explains my idea more clearly. The goal is to generate endpoint specific swagger definitions even if they derive from the same serializer. |
Okay, I see your point. I still don't think that would be reasonable default behaviour, but I could see its utility as a feature enabed by a setting or class attribute. I'd be open to merging a PR implementing this. |
@axnsan12 - Any update on this feature? |
As far as I know no one has started work on it. |
And can drf-yasg automatically generate the spec from these serializers ? I don't see how it would be possible. |
I encountered the same issue - write_only fields are still present in the generated schema. As for the read and write serializers, they are not supported, so in my case I needed to decorize the view with: @swagger_auto_schema(
responses={201: CreateSerializer},
)
def post(self, request, *args, **kwargs): Of course it would be nice to have automatic discovery of read and write serializers, but I'm not sure how to achieve that since DRF doesn't recognize |
This is what I ended up doing https://github.com/Polyconseil/django-mds/blob/master/mds/apis/utils.py#L70-L81 With a customized schema generator https://github.com/Polyconseil/django-mds/blob/master/mds/apis/utils.py#L136-L213 Not ideal at all :( but it more or less works |
Also encountered this issue. I ended up creating an AutoSchema that creates a class ReadOnly():
def get_fields(self):
new_fields = OrderedDict()
for fieldName, field in super().get_fields().items():
if not field.write_only:
new_fields[fieldName] = field
return new_fields
class WriteOnly():
def get_fields(self):
new_fields = OrderedDict()
for fieldName, field in super().get_fields().items():
if not field.read_only:
new_fields[fieldName] = field
return new_fields
class BlankMeta:
pass
class ReadWriteAutoSchema(SwaggerAutoSchema):
def get_view_serializer(self):
return self._convert_serializer(WriteOnly)
def get_default_response_serializer(self):
body_override = self._get_request_body_override()
if body_override and body_override is not no_body:
return body_override
return self._convert_serializer(ReadOnly)
def _convert_serializer(self, new_class):
serializer = super().get_view_serializer()
if not serializer:
return serializer
class CustomSerializer(new_class, serializer.__class__):
class Meta(getattr(serializer.__class__, 'Meta', BlankMeta)):
ref_name = new_class.__name__ + serializer.__class__.__name__
new_serializer = CustomSerializer(data=serializer.data)
return new_serializer |
Thanks a lot, it works, only one issue I've found, it doesn't work with custom schema generated using decorator |
I have tried fixing this issue by writing a view mixin: class PartialUpdateModelMixin:
"""
Update a model instance in patch mode.
Why not rest_framework.mixins.UpdateModelMixin?
Because it bundles "update" and "partial_update" together,
and I only need the latter.
"""
def get_serializer(self, *args, **kwargs):
"""
Remove required option when the serializer is used for partial update.
See also https://github.com/axnsan12/drf-yasg/issues/70
and https://github.com/axnsan12/drf-yasg/issues/459
"""
serializer = super().get_serializer(*args, **kwargs)
if self.action == 'partial_update':
_orig_get_fields = serializer.get_fields
def get_fields_wrapper():
fields = _orig_get_fields()
for n, f in fields.items():
f.required = False
return fields
serializer.get_fields = get_fields_wrapper
return serializer
def partial_update(self, request, *args, **kwargs):
partial = True
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save() Then use it in your viewset: class SomeModelViewSet(PartialUpdateModelMixin, GenericViewSet):
pass Hope it helps you! |
add this to your settings.py
|
A pain point that often exists with automatic swagger specs with DRF is they reuse the same definition for retrieve/list/post endpoints. This leads to painful client side SDKs generated with
swagger-codegen
as most/all fields on the resulting model will be optional, requiring SDK consumers to litter their code with null checks (or ignore them at their peril).A motivating example of this problem is the
id
field that most django models will have. This field isrequired
on reads (the model will always have an id when retrieving/listing) but aread_only
field for creates (not required for post requests). Another example would be a field that is derrives from anull=False
model field (required at the DB level) but is not required at the API level (perhaps it's generated by the server's business logic).Can (does)
drf-yasg
's inspection of serializers find these characteristics and if so, can it be shown in the resulting spec by defining multiple definitions for a serializer and referencing them appropriately in spec paths?For context, our team generates SDKs for our APIs in many type safe languages and model definitions such as:
are a headache to use because the resulting model will label
id
as optional, meaning it must be checked before being passed to other SDK methods that expect a value. E.g.The text was updated successfully, but these errors were encountered: