wakey
wakey

Reputation: 2409

Django Rest Framework - call serializer.to_internal_value on a null value

I am attempting to write a Field Serializer for my django API that passes null to a custom .to_internal_value method.

My use case is for two datetime fields that make up a time range. I want a user of my API to be able to give me something along the lines of "start_dt": null in a PUT/POST/PATCH request and have my serializer translate that into datetime.datetime.min (or, in the case of end_dt, datetime.datetime.max).

Here is what I have so far. I am able to get Object->JSON serialization to work well (translates datetime.min and datetime.max values to None) using the .to_representation(self, value) method - however it does not seem to call .to_internal_value(self, value) when converting from null->None - and instead passes None through to the rest of my logic whenever one of the two fields is given as null.

import pytz
from datetime import datetime

UTC = pytz.timezone("UTC")
max_dt = UTC.localize(datetime.max)
min_dt = UTC.localize(datetime.min)

class CustomDateTimeField(serializers.Field):

    def __init__(self, *args, **kwargs):

        self.datetime_field = fields.DateTimeField()

        self.infinity_direction = kwargs.pop('infinity_direction', None)
        if not (self.infinity_direction == '+' or self.infinity_direction == '-'):
            raise AssertionError("Expected CustomDateTimeField to be created with `infinity_direction` equal to either '+' or '-'")

        super().__init__(*args, **kwargs)

    def to_representation(self, value):
        """
        Represent both MAX and MIN times as None/'null'
        Else return a normal datetime string
        """

        if value == min_dt or value == max_dt:
            return None

        return self.datetime_field.to_representation(value)


    def to_internal_value(self, value):
        """
        Translate None/'null' values to either MAX or MIN - depending on self.infinity_direction
        Else return a normal datetime object
        """

        if value == None:
            return max_dt if self.infinity_direction == '+' else min_dt

        return self.datetime_field.to_internal_value(value)

In my Model's serializer I have these fields in use like...

class TimeRangeSerializer(serializers.ModelSerializer):

    start_dt = CustomDateTimeField(infinity_direction='-', allow_null=True)
    end_dt = CustomDateTimeField(infinity_direction='+', allow_null=True)

    class Meta:
        model = TimeRange
        fields = ('id', 'start_dt', 'end_dt')

To provide some more examples...

EX 1: Posting this

{}

gives the following correct return value

{
    "end_dt": [
        "This field is required."
    ],
    "start_dt": [
        "This field is required."
    ]

}

EX 2:

Posting this

{
    "start_dt": null,
    "end_dt": null
}

CustomDateTimeField.to_internal_value never gets called - and start_dt and end_dt get passed to my object's creation as None, instead of the desired datetime.min and datetime.max values.

EX 3:

Posting this

{
    "start_dt": "2018-08-28T00:00:00Z",
    "end_dt": null
}

Seems to call to_internal_value - but only for start_dt (which it resolves correctly into a datetime object). It does not call to_internal_value on the null.

Upvotes: 2

Views: 4715

Answers (2)

Ehsan Nouri
Ehsan Nouri

Reputation: 2040

change your code as below:

def to_internal_value(self, value):
    """
    Translate None/'null' values to either MAX or MIN - depending on self.infinity_direction
    Else return a normal datetime object
    """

    return self.datetime_field.to_internal_value(value)

def validate_empty_values(self, data):
    (is_empty_value, data) = super(CustomDateTimeField, self).validate_empty_values(data)

    if is_empty_value and data is None:
        return is_empty_value, max_dt if self.infinity_direction == '+' else min_dt

    return is_empty_value, data

drf never calls the to_interval_value of a field if its value is empty.

Upvotes: 3

Raydel Miranda
Raydel Miranda

Reputation: 14360

It seems the framework don't bother in trying to de-serialize a field that is 'null' in the JSON, since no matter what Field type you're dealing with, 'null' will always be de-serialize to None.

You have a very special use case, but hey, you're lucky, you're the one writing the API. Just tell the user/s of your API that has to use some placeholder for that, for instance: min_date_limit or max_date_limit.

If you detect one of those values in the to_internal_value function, well, you know what to do.

Upvotes: 0

Related Questions