Django-rest-framework: A way for model fields to specify what DRF field to use

Created on 7 Aug 2015  路  16Comments  路  Source: encode/django-rest-framework

This issue is to propose a way that a custom model field can define what serializer field it should use. For example add a class attribute to the new field.

The use case I see for this is: I create a reusable package that creates some new model fields. I want to include custom DRF serializer fields for them. I want to be able to, within the package, tell DRF to use my custom serializer field. I do not want all the users of my package to have to override the ModelSerializer or manually specify the serializer field every time they use it.

That is the general idea for it. I'd be interested to hear what you think.

As an example of how I envision this might be used:

class CustomURLSerializerField(serializers.URLField):
    pass

class CustomURLField(models.URLField):
    rest_framework_field = CustomURLSerializerField

Most helpful comment

@tomchristie

This can be useful in some scenarios. For example, the default DurationField renders the duration as a string that looks like HH:MM:SS.mmmm. If I want to change it to render total seconds or milliseconds, my options are,

  • Specify field on every serializer
    We know this is not viable for big projects.

  • Override the mapping on a BaseSerializer and inherit from it everywhere
    This could work but I feel it is better to avoid inheritance unless absolutely necessary. Sometimes it gets a little hard to manage when dealing with a lot of base classes.

I would personally prefer a global mapping in django settings that overrides the local serializer class mapping. This patter would fit well with existing DRF APIs like authentication, permission, renderers, datetime formatting etc.

Defining something like the following in settings should

from django.db.models import fields
from custom_lib.fields import DurationField

REST_FRAMEWORK_FIELD_MAPPING = {
    fields.DurationField: DurationField
}

This could also be useful if some 3rd party apps ship serializers and you need to them to use a different serializer field for a certain model fields globally.

Also, sub-classing from a custom class adds some overhead to the process in really big projects with lots of engineers.

All 16 comments

@RyanPineo You can do that by adding your Django field / DRF field mapping to the ModelSerializer .serializer_field_mapping (see https://github.com/tomchristie/django-rest-framework/blob/8d7c0a8474583df6b27d0d5be8a84bd9a154ab35/rest_framework/serializers.py#L750)

@xordoquy wouldn't that mean that you would have to define a new serializer class that inerhits from ModelSerializer and redefine serializer_field_mapping, then change the base class of every one of your serializers that uses a model serializer?

The main goal of this idea was so that a 3rd party package could create it's own mapping without having to create it's own ModelSerializer subclass or monkey patch ModelSerializer.

You may but it's simplier of you alter the ModelSerializer so it's transparent to the user

So the recommended way is to monkey patch ModelSerializer?

No, the recommended way is to just specify the field on the serializer class - not a big deal.

I do think that we _might_ consider something like this in the future (eg defer to the value of a serializer_field_class attribute if one exists) but not overwhelmed by a pressing need for it right now.

Specifically which third party packages would make use of this right now if it was available and what fields would they map too?

So the recommended way is to monkey patch ModelSerializer?

As Tom said, the recommended way is to manually specify the field.

But if you _want_ to have it automatically mapped, you can monkey patch ModelSerializer. That's what the GIS fields package does and it appears to be working well.

Taking a look at the GiS package, it looks like it could make great use of an API like this. Instead of monkey patching (which I think is generally frowned upon) there would be a documented and known way to do this kind of thing that 3rd party packages can take advantage of.

The original motivation was for my own personal work where we are working on quite a few projects at any one time that often have similar needs. It would be useful to put that common functionality in a package with model fields and serializer fields and have them mapped without needing to manually do it in every project that uses the package.

@RyanPineo Monkey patching is different from what the alternative we provided here.
@tomchristie what about providing a public entry point to alter the default fields / add new mappings ? We've already seen this question twice before this one.

It is already easy to support this in a codebase by override ModelSerializer and subclassing serializer_field_mapping and/or get_fields() - then use that serializer class as your base throughout.

Alternatively we do construct automatic fields based on unknown model fields, which may be sufficent in some cases, or of course, just add the field explicitly.

However there would be _some_ value in third parties being able to automatically provide the mapping themselves (although it comes at a cost of an extra indirection, and closer coupling - at the moment serializers "know" about mapping from model fields, but not the other way around.)

I'd consider this if it was approached from a standpoint of demonstrating _exactly_ which third party packages would take advantage of this, and _exactly_ what the difference in their usage/behavior would then be.

@tomchristie

This can be useful in some scenarios. For example, the default DurationField renders the duration as a string that looks like HH:MM:SS.mmmm. If I want to change it to render total seconds or milliseconds, my options are,

  • Specify field on every serializer
    We know this is not viable for big projects.

  • Override the mapping on a BaseSerializer and inherit from it everywhere
    This could work but I feel it is better to avoid inheritance unless absolutely necessary. Sometimes it gets a little hard to manage when dealing with a lot of base classes.

I would personally prefer a global mapping in django settings that overrides the local serializer class mapping. This patter would fit well with existing DRF APIs like authentication, permission, renderers, datetime formatting etc.

Defining something like the following in settings should

from django.db.models import fields
from custom_lib.fields import DurationField

REST_FRAMEWORK_FIELD_MAPPING = {
    fields.DurationField: DurationField
}

This could also be useful if some 3rd party apps ship serializers and you need to them to use a different serializer field for a certain model fields globally.

Also, sub-classing from a custom class adds some overhead to the process in really big projects with lots of engineers.

@owais So the right approach if anyone wants to take this on, would be to implement a different ModelSerializer base class, and ship that as a third party package, that we can link to from the docs.

The ModelSerializer class has all the hooks that you'd need in order to implement this, and third party packages are a great way to build in extra functionality that might not yet be ready for core.

@owais alternatively, you could also override the ModelSerializer.serializer_field_mappingas done in serializers.py

if hasattr(models, 'UUIDField'):
    ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField

If you choose this option, make sure it's in a place that's easy to find.

@xordoquy That is what I'm doing in a subsection of one of my big projects. I was looking for a way to override mapping globally so that all API endpoints use the special field. Looks like I'll have to monkey patch the base serializer to make the change truly global but I'm really trying to avoid that. The reason I don't want to inherit from a custom class is that the project has tons of serializers and lots of engineers. It's hard to enforce that everyone must subclass a certain custom base serializer or add a mixin. Only way I can think of enforcing this is to use a thin wrapped/forked DRF, write custom lint rules or patch base classes at run time.

... would be to implement a different ModelSerializer base class, and ship that as a third party package... and third party packages are a great way to build in extra functionality that might not yet be ready for core.

@tomchristie Sound very reasonable. I'll give it a shot as soon as I need to use the new field more broadly in one of my projects.

Wrt. date fields specifically, a different route to take would be to set DATETIME_FORMAT = None in your settings (so that datetime fields return datetime instances) and use custom JSON renderer (also configured in your global settings) that handles the datetime serialization into whichever format you want. That way everything would be configured for the project as a whole.

Please see my comment here: https://github.com/hzdg/django-enumfields/issues/30

Not sure it's a compelling use case -- one could probably accomplish the same behaviour with a custom JSON renderer that handles enums.

Way to make this happen: demonstrate two or more third party modelfield packages that'd want to use this. Demo what API representations their fields currently get (or what datatype they output that we don't support) and make the case that it'd improve the experience of using those packages. More than happy to be convinced by this one.

Was this page helpful?
0 / 5 - 0 ratings