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

In a OpenApiSerializerFieldExtension, how to extend from the default output? #426

Closed
foucdeg opened this issue Jun 10, 2021 · 9 comments
Closed

Comments

@foucdeg
Copy link

foucdeg commented Jun 10, 2021

In the example provided for OpenApiSerializerFieldExtension, the override simply returns a basic type.

The customization we would like to do is extend from the default output for the field, keeping description which is derived from the Django model field's help_text but adding an example.

It would look like this:

class DateFieldFix(OpenApiSerializerFieldExtension):
    target_class = "rest_framework.fields.DateField"

    def map_serializer_field(self, auto_schema, direction):
        base = self.get_default_output() # invented method here, does something like that exist?
        base["example"] = "2021-06-10"

        return base

Sorry for asking a lot of questions in a short time, but your quick replies have encouraged me to ask more questions :)

@foucdeg foucdeg changed the title In a OpenApiSerializerFieldExtension, how to extend the default output? In a OpenApiSerializerFieldExtension, how to extend from the default output? Jun 10, 2021
@tfranzel
Copy link
Owner

i'm sry but you now used up your free questions. this is for legitimate issues and bugs, and i'm not stackoverflow. your other issue #425 indicated that you did not even try to find out yourself. i'm happy to help with tricky issues, but only after i have seen some effort on your part. i have only limited time doing this for free.

    def map_serializer_field(self, auto_schema, direction):
        schema = auto_schema._map_serializer_field(self.target, direction)
        schema["example"] = "2021-06-10"
        return schema

@foucdeg
Copy link
Author

foucdeg commented Jun 10, 2021

Thanks for the answer.

I assure you that I have spent already one full working day setting up this library and converting all of our existing customizations to it. Maybe I'm dumb, maybe the task was a bit ambitious, but there is no lack of effort. And I'm getting close.

If you're interested in my attempts before opening the issue, I did try super().map_serializer_field(auto_schema, direction) but there was no such method in OpenApiSerializerFieldExtension. In the code of that class which I did read, the extension mechanism wasn't clear enough for me to guess that there would be a similarly named method on AutoSchema.

I would humbly suggest updating the doc for OpenApiSerializerFieldExtension, with this more full-featured example.

@foucdeg foucdeg closed this as completed Jun 10, 2021
tfranzel added a commit that referenced this issue Jun 10, 2021
@foucdeg
Copy link
Author

foucdeg commented Jun 11, 2021

@tfranzel actually, this solution creates an infinite loop because AutoSchema._map_serializer_field calls the extension override again:

File "/webapp/citymeo/api/serializers.py", line 69, in map_serializer_field
    base = auto_schema._map_serializer_field(self.target, direction)
File "/usr/local/lib/python3.7/dist-packages/drf_spectacular/openapi.py", line 471, in _map_serializer_field
    schema = serializer_field_extension.map_serializer_field(self, direction)
File "/webapp/citymeo/api/serializers.py", line 69, in map_serializer_field
    base = auto_schema._map_serializer_field(self.target, direction)

This is my extension code:

class HyperlinkedRelatedFieldFix(OpenApiSerializerFieldExtension):
    target_class = "rest_framework.relations.HyperlinkedRelatedField"

    def map_serializer_field(self, auto_schema, direction):
        base = auto_schema._map_serializer_field(self.target, direction)
        base["format"] = "uri"
        base["example"] = "https://{}/api/management/v2/<objecttype>/<objectid>".format(
            settings.DEFAULT_DOMAIN
        )
        return base

I'll try to find a workaround, but for future readers here, don't just copy-paste that solution.

@foucdeg
Copy link
Author

foucdeg commented Jun 11, 2021

Workaround:

class HyperlinkedRelatedFieldFix(OpenApiSerializerFieldExtension):
    target_class = "rest_framework.relations.HyperlinkedRelatedField"

    def map_serializer_field(self, auto_schema, direction):
        # temporarily "disable" this extension to prevent running it again when calling the AutoSchema method
        HyperlinkedRelatedFieldFix.target_class = None
        base = auto_schema._map_serializer_field(self.target, direction)
        # reenable this extension
        HyperlinkedRelatedFieldFix.target_class = "rest_framework.relations.HyperlinkedRelatedField"
        base["format"] = "uri"
        base["example"] = "https://{}/api/management/v2/<objecttype>/<objectid>".format(
            settings.DEFAULT_DOMAIN
        )
        return base

@tfranzel
Copy link
Owner

oh right. sry about that. i didn't think of that. probably the reason why i didn't do that myself anywhere. i'll think about something better.

@foucdeg
Copy link
Author

foucdeg commented Jun 13, 2021

Here's a cleaner subclass that can be extended instead of OpenApiSerializerFieldExtension:

class SerializerFieldExtensionWithOverride(OpenApiSerializerFieldExtension):
    def get_default_output(self, auto_schema, direction):
        child_cls = type(self)
        backup_target_class = child_cls.target_class
        child_cls.target_class = None
        base_schema = auto_schema._map_serializer_field(self.target, direction)
        child_cls.target_class = backup_target_class

        return base_schema

Usage:

class NullBooleanFieldFix(SerializerFieldExtensionWithOverride):
    target_class = "rest_framework.fields.NullBooleanField"

    def map_serializer_field(self, auto_schema, direction):
        defaults = self.get_default_output(auto_schema, direction)
        defaults["nullable"] = True
        return defaults

Are you interested in a PR to add the get_default_output method to the base OpenApiSerializerFieldExtension ?

@tfranzel
Copy link
Owner

hey @foucdeg, that certainly works and is a nifty idea, although not the cleanest solution imho.

it's probably better to run the method but indicate that extensions should not be entered. maybe we should add a parameter to map_serializer and map_serializer_field like so:

auto_schema._map_serializer_field(self.target, direction, bypass_extensions=True)

that way we don't have to temporarily hack the extension. that do you think?

@foucdeg
Copy link
Author

foucdeg commented Jun 14, 2021

I'd mix both solutions:

  • still add a get_default_output method on OpenApiSerializerFieldExtension
  • but instead of hacking the extension, it just calls auto_schema._map_serializer_field with your proposed bypass_extensions flag which is indeed cleaner.

The new method has the advantage of being more explicit / self-documented for users trying to figure out how to do what I needed to do.

What do you think @tfranzel ?

@tfranzel
Copy link
Owner

so i took your comment into consideration, but decided against having both. i only implemented bypass_extensions and added this to the documentation. in any case, you should now be able to achieve what you wanted.

the reason is that in nearly half of the cases this will not work anyway due warnings in the regular codepath. also get_default_output would be the only convenience method in all extensions. it would also only contain that one line that is documented in the docstring. so basically fort consistency and brevity i decided against it.

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

No branches or pull requests

2 participants