Add example using source=‘*’ to custom field docs. (#5688)

* Add example using `source=‘*’` to custom field docs.
* Add nested serialiser example

Closes #2032 closes #3066
This commit is contained in:
Carlton Gibson 2017-12-20 10:04:12 +01:00 committed by GitHub
parent d38b94fd74
commit cf3929d88d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -561,6 +561,8 @@ Note that the `WritableField` class that was present in version 2.x no longer ex
## Examples
### A Basic Custom Field
Let's look at an example of serializing a class that represents an RGB color value:
class Color(object):
@ -600,7 +602,7 @@ As an example, let's create a field that can be used to represent the class name
"""
return obj.__class__.__name__
#### Raising validation errors
### Raising validation errors
Our `ColorField` class above currently does not perform any data validation.
To indicate invalid data, we should raise a `serializers.ValidationError`, like so:
@ -646,6 +648,137 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me
This style keeps your error messages cleaner and more separated from your code, and should be preferred.
### Using `source='*'`
Here we'll take an example of a _flat_ `DataPoint` model with `x_coordinate` and `y_coordinate` attributes.
class DataPoint(models.Model):
label = models.CharField(max_length=50)
x_coordinate = models.SmallIntegerField()
y_coordinate = models.SmallIntegerField()
Using a custom field and `source='*'` we can provide a nested representation of
the coordinate pair:
class CoordinateField(serializers.Field):
def to_representation(self, obj):
ret = {
"x": obj.x_coordinate,
"y": obj.y_coordinate
}
return ret
def to_internal_value(self, data):
ret = {
"x_coordinate": data["x"],
"y_coordinate": data["y"],
}
return ret
class DataPointSerializer(serializers.ModelSerializer):
coordinates = CoordinateField(source='*')
class Meta:
model = DataPoint
fields = ['label', 'coordinates']
Note that this example doesn't handle validation. Partly for that reason, in a
real project, the coordinate nesting might be better handled with a nested serialiser
using `source='*'`, with two `IntegerField` instances, each with their own `source`
pointing to the relevant field.
The key points from the example, though, are:
* `to_representation` is passed the entire `DataPoint` object and must map from that
to the desired output.
>>> instance = DataPoint(label='Example', x_coordinate=1, y_coordinate=2)
>>> out_serializer = DataPointSerializer(instance)
>>> out_serializer.data
ReturnDict([('label', 'testing'), ('coordinates', {'x': 1, 'y': 2})])
* Unless our field is to be read-only, `to_internal_value` must map back to a dict
suitable for updating our target object. With `source='*'`, the return from
`to_internal_value` will update the root validated data dictionary, rather than a single key.
>>> data = {
... "label": "Second Example",
... "coordinates": {
... "x": 3,
... "y": 4,
... }
... }
>>> in_serializer = DataPointSerializer(data=data)
>>> in_serializer.is_valid()
True
>>> in_serializer.validated_data
OrderedDict([('label', 'Second Example'),
('y_coordinate', 4),
('x_coordinate', 3)])
For completeness lets do the same thing again but with the nested serialiser
approach suggested above:
class NestedCoordinateSerializer(serializers.Serializer):
x = serializers.IntegerField(source='x_coordinate')
y = serializers.IntegerField(source='y_coordinate')
class DataPointSerializer(serializers.ModelSerializer):
coordinates = NestedCoordinateSerializer(source='*')
class Meta:
model = DataPoint
fields = ['label', 'coordinates']
Here the mapping between the target and source attribute pairs (`x` and
`x_coordinate`, `y` and `y_coordinate`) is handled in the `IntegerField`
declarations. It's our `NestedCoordinateSerializer` that takes `source='*'`.
Our new `DataPointSerializer` exhibits the same behaviour as the custom field
approach.
Serialising:
>>> out_serializer = DataPointSerializer(instance)
>>> out_serializer.data
ReturnDict([('label', 'testing'),
('coordinates', OrderedDict([('x', 1), ('y', 2)]))])
Deserialising:
>>> in_serializer = DataPointSerializer(data=data)
>>> in_serializer.is_valid()
True
>>> in_serializer.validated_data
OrderedDict([('label', 'still testing'),
('x_coordinate', 3),
('y_coordinate', 4)])
But we also get the built-in validation for free:
>>> invalid_data = {
... "label": "still testing",
... "coordinates": {
... "x": 'a',
... "y": 'b',
... }
... }
>>> invalid_serializer = DataPointSerializer(data=invalid_data)
>>> invalid_serializer.is_valid()
False
>>> invalid_serializer.errors
ReturnDict([('coordinates',
{'x': ['A valid integer is required.'],
'y': ['A valid integer is required.']})])
For this reason, the nested serialiser approach would be the first to try. You
would use the custom field approach when the nested serialiser becomes infeasible
or overly complex.
# Third party packages
The following third party packages are also available.