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 ## Examples
### A Basic Custom Field
Let's look at an example of serializing a class that represents an RGB color value: Let's look at an example of serializing a class that represents an RGB color value:
class Color(object): 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__ return obj.__class__.__name__
#### Raising validation errors ### Raising validation errors
Our `ColorField` class above currently does not perform any data validation. Our `ColorField` class above currently does not perform any data validation.
To indicate invalid data, we should raise a `serializers.ValidationError`, like so: 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. 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 # Third party packages
The following third party packages are also available. The following third party packages are also available.