Drf-yasg: Provide an example to documentation for custom `Serializer Meta nested class`

Created on 22 Oct 2019  路  10Comments  路  Source: axnsan12/drf-yasg

Currently as of 0.17.0, the docs section https://drf-yasg.readthedocs.io/en/stable/custom_spec.html#serializer-meta-nested-class could really use an example.

For example, it's not immediately clear what should go into the swagger_schema_fields dictionary and how that will be translated into the resulting schema. An example (perhaps with a simple, nested Schema) would go a long way.

All 10 comments

The only example I could find in the project is: https://github.com/axnsan12/drf-yasg/pull/134/files#diff-d9704604715b5f380cefb3b49a04462bR31

What I'm trying to do is override the default generated OpenAPI schema for one particular JSONField from object to a custom representation (an object with a few required properties).

Perhaps @etene you have some ideas related to #417?

Something like:

```py
class Email(models.Model):
# Store data as JSON, but the data should be made up of
# an object that has two properties, "subject" and "body"
# Example:
# {
# "subject": "My Title",
# "body": "The body of the message.",
# }
message = JSONField()

class EmailSerializer(ModelSerializer):
class Meta:
model = Email
fields = '__all__'

   # Want to declare in the OpenAPI schema that message
   # should have subject and body properties.
    swagger_schema_fields = {
        # Getting schema validation error:
        #   drf_yasg.errors.SwaggerValidationError: spec validation failed: {'ssv': '("\'message\' does not match any of the regexes:
        'message': openapi.Schema(
            type=openapi.TYPE_OBJECT,
            ...
        )
    }

Off the top of my head, this would be done using a custom serializer for your email messages.

This way:

  • DRF can validate that your json contains the required fields
  • drf-yasg can build a Schema by introspecting the serializer

Something like this:

class EmailMessageSerializer(serializers.Serializer):
    subject = serializers.CharField()
    body = serializers.CharField()

class EmailSerializer(ModelSerializer):
    class Meta:
        model = Email
        fields = '__all__'

    message = EmailMessageSerializer()

Of course it won't work as is, I didn't test it, but you get the idea.

@etene Thanks for the idea. I should have noted this in the first post, but I originally did use a nested serializers but there was a big performance (in the production, not this toy example) penalty for not collapsing into a JSONField.

So I was hoping to use a JSONField but simply patch the Schema created by drf-yasg so to outside users the API remained the same.

Ok, I didn't know performance was an issue here.
What I would do in this case is use a custom serializer field, like this:

class EmailMessageField(serializers.JSONField):
    class Meta:
        swagger_schema_fields = {
            "properties": {
                "subject": openapi.Schema(
                    title="Email subject",
                    type=openapi.TYPE_STRING,
                ),
                "body": openapi.Schema(
                    title="Email body",
                    type=openapi.TYPE_STRING,
                ),
            }
         }

class EmailSerializer(ModelSerializer):
    class Meta:
        model = Email
        fields = '__all__'

    message = EmailMessageField()

Which would output, for the message field:

{
  "title": "Message",
  "type": "object",
  "properties": {
    "subject": {
      "title": "Email subject",
      "type": "string"
    },
    "body": {
      "title": "Email body",
      "type": "string"
    }
  }
}

It's still a regular JSON field from DRF's point of view, which means no extra validation, but you can customize its fields so drf-yasg returns the info you want.

@etene Thanks this was exactly the example I was looking for!

Thank you!

@etene Hmm, the only snag with this method is that it seems like if there are custom validators on the Model:

from django.contrib.postgres.fields import JSONField

class Email(models.Model):
    message = JSONField(validators=[MyCustomValidator()])

class EmailMessageField(serializers.JSONField):
    class Meta:
        swagger_schema_fields = {
            "properties": {
                "subject": openapi.Schema(
                    title="Email subject",
                    type=openapi.TYPE_STRING,
                ),
                "body": openapi.Schema(
                    title="Email body",
                    type=openapi.TYPE_STRING,
                ),
            }
         }

class EmailSerializer(ModelSerializer):
    class Meta:
        model = Email
        fields = '__all__'

    message = EmailMessageField()

It seems like by overriding the message field in the ModelSerializer that the default Model validation no longer occurs (MyCustomValidator() is never called)?

Do you know if there is a way to restore this behavior?

Yes, I was surprised at first but I dug deeper into how it works and it's normal behavior.

In the regular case (without any custom field), DRF uses the EmailSerializer's serializer_field_mapping to determine which models fields correspond to which serializer fields. This mapping inherits the base serializer field mapping defined by DRF.

In that case, the generated serializer field looks like this:

message = JSONField(encoder=None, style={'base_template': 'textarea.html'}, validators=[<function my_custom_validator>])

because the introspection code in the serializer's build_standard_field method determines which options & validators apply.

But in our case, the field isn't built automatically anymore because we instantiate it directly in the serializer (message = EmailMessageField()) without passing it any options.


So we have two options here if we want to keep the validation:

Option 1: custom field mapping

Override the JSONField key in EmailSerializer.serializer_field_mapping, to point to EmailMessageField, so when DRF encounters a JSONField on the model it will use our custom field but run introspection code and correctly attach the validator.
This implies that every instance of JSONField on the Email model will generate an EmailMessageField.

Example:

class EmailSerializer(serializers.ModelSerializer):
    class Meta:
        model = Email
        fields = '__all__'

    serializer_field_mapping = {
        **serializers.ModelSerializer.serializer_field_mapping,
        JSONField: EmailMessageField,
    }

Option 2: pass validator explicitely

This is a bit simpler, and the custom validator will only apply to our field.
However, other options inferred by DRF's (such as encoder & style) will not be present because we'll bypass build_standard_field again.

class EmailSerializer(serializers.ModelSerializer):
    class Meta:
        model = Email
        fields = '__all__'

    message = EmailMessageField(validators=[my_custom_validator])

I hope this has been clear enough and solves your problem, if not don't hesitate and we'll dig even deeper !

@etene Perfect.

I took Option 2 one step further using default_validators:

class EmailMessageField(serializers.JSONField):
    default_validators = [my_custom_validator]

    class Meta:
        swagger_schema_fields = {
            "properties": {
                "subject": openapi.Schema(
                    title="Email subject",
                    type=openapi.TYPE_STRING,
                ),
                "body": openapi.Schema(
                    title="Email body",
                    type=openapi.TYPE_STRING,
                ),
            }
         }

Because my validator was used in several places, and this allowed me not have to include it for each Serializer the Field was used on.

Thanks! I'll see if I can create a PR that uses your example as a way to better document swagger_schema_fields for others in the future.

Yes, I briefly thought about setting the validator as a class attribute when writing my long winded comment but then I forgot. That's even better IMHO. Glad I could help you anyway !

As for the PR, I guess an example of using swagger_schema_fields to customize a serializer Field just like we did here would be great. The relevant documentation is indeed pretty terse and makes it look like it is only relevant for Serializers. If you do that, don't forget to mention the validator caveat :wink:
If you need any help with that PR, just ping me, I'll be happy to help.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

csdenboer picture csdenboer  路  3Comments

agethecoolguy picture agethecoolguy  路  4Comments

geekashu picture geekashu  路  5Comments

Safrone picture Safrone  路  3Comments

gertjanol picture gertjanol  路  5Comments