Reputation: 2409
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
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
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