Skip to content

Commit cf3929d

Browse files
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
1 parent d38b94f commit cf3929d

File tree

1 file changed

+134
-1
lines changed

1 file changed

+134
-1
lines changed

β€Ždocs/api-guide/fields.md

+134-1
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,8 @@ Note that the `WritableField` class that was present in version 2.x no longer ex
561561

562562
## Examples
563563

564+
### A Basic Custom Field
565+
564566
Let's look at an example of serializing a class that represents an RGB color value:
565567

566568
class Color(object):
@@ -600,7 +602,7 @@ As an example, let's create a field that can be used to represent the class name
600602
"""
601603
return obj.__class__.__name__
602604

603-
#### Raising validation errors
605+
### Raising validation errors
604606

605607
Our `ColorField` class above currently does not perform any data validation.
606608
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
646648

647649
This style keeps your error messages cleaner and more separated from your code, and should be preferred.
648650

651+
### Using `source='*'`
652+
653+
Here we'll take an example of a _flat_ `DataPoint` model with `x_coordinate` and `y_coordinate` attributes.
654+
655+
class DataPoint(models.Model):
656+
label = models.CharField(max_length=50)
657+
x_coordinate = models.SmallIntegerField()
658+
y_coordinate = models.SmallIntegerField()
659+
660+
Using a custom field and `source='*'` we can provide a nested representation of
661+
the coordinate pair:
662+
663+
class CoordinateField(serializers.Field):
664+
665+
def to_representation(self, obj):
666+
ret = {
667+
"x": obj.x_coordinate,
668+
"y": obj.y_coordinate
669+
}
670+
return ret
671+
672+
def to_internal_value(self, data):
673+
ret = {
674+
"x_coordinate": data["x"],
675+
"y_coordinate": data["y"],
676+
}
677+
return ret
678+
679+
680+
class DataPointSerializer(serializers.ModelSerializer):
681+
coordinates = CoordinateField(source='*')
682+
683+
class Meta:
684+
model = DataPoint
685+
fields = ['label', 'coordinates']
686+
687+
Note that this example doesn't handle validation. Partly for that reason, in a
688+
real project, the coordinate nesting might be better handled with a nested serialiser
689+
using `source='*'`, with two `IntegerField` instances, each with their own `source`
690+
pointing to the relevant field.
691+
692+
The key points from the example, though, are:
693+
694+
* `to_representation` is passed the entire `DataPoint` object and must map from that
695+
to the desired output.
696+
697+
>>> instance = DataPoint(label='Example', x_coordinate=1, y_coordinate=2)
698+
>>> out_serializer = DataPointSerializer(instance)
699+
>>> out_serializer.data
700+
ReturnDict([('label', 'testing'), ('coordinates', {'x': 1, 'y': 2})])
701+
702+
* Unless our field is to be read-only, `to_internal_value` must map back to a dict
703+
suitable for updating our target object. With `source='*'`, the return from
704+
`to_internal_value` will update the root validated data dictionary, rather than a single key.
705+
706+
>>> data = {
707+
... "label": "Second Example",
708+
... "coordinates": {
709+
... "x": 3,
710+
... "y": 4,
711+
... }
712+
... }
713+
>>> in_serializer = DataPointSerializer(data=data)
714+
>>> in_serializer.is_valid()
715+
True
716+
>>> in_serializer.validated_data
717+
OrderedDict([('label', 'Second Example'),
718+
('y_coordinate', 4),
719+
('x_coordinate', 3)])
720+
721+
For completeness lets do the same thing again but with the nested serialiser
722+
approach suggested above:
723+
724+
class NestedCoordinateSerializer(serializers.Serializer):
725+
x = serializers.IntegerField(source='x_coordinate')
726+
y = serializers.IntegerField(source='y_coordinate')
727+
728+
729+
class DataPointSerializer(serializers.ModelSerializer):
730+
coordinates = NestedCoordinateSerializer(source='*')
731+
732+
class Meta:
733+
model = DataPoint
734+
fields = ['label', 'coordinates']
735+
736+
Here the mapping between the target and source attribute pairs (`x` and
737+
`x_coordinate`, `y` and `y_coordinate`) is handled in the `IntegerField`
738+
declarations. It's our `NestedCoordinateSerializer` that takes `source='*'`.
739+
740+
Our new `DataPointSerializer` exhibits the same behaviour as the custom field
741+
approach.
742+
743+
Serialising:
744+
745+
>>> out_serializer = DataPointSerializer(instance)
746+
>>> out_serializer.data
747+
ReturnDict([('label', 'testing'),
748+
('coordinates', OrderedDict([('x', 1), ('y', 2)]))])
749+
750+
Deserialising:
751+
752+
>>> in_serializer = DataPointSerializer(data=data)
753+
>>> in_serializer.is_valid()
754+
True
755+
>>> in_serializer.validated_data
756+
OrderedDict([('label', 'still testing'),
757+
('x_coordinate', 3),
758+
('y_coordinate', 4)])
759+
760+
But we also get the built-in validation for free:
761+
762+
>>> invalid_data = {
763+
... "label": "still testing",
764+
... "coordinates": {
765+
... "x": 'a',
766+
... "y": 'b',
767+
... }
768+
... }
769+
>>> invalid_serializer = DataPointSerializer(data=invalid_data)
770+
>>> invalid_serializer.is_valid()
771+
False
772+
>>> invalid_serializer.errors
773+
ReturnDict([('coordinates',
774+
{'x': ['A valid integer is required.'],
775+
'y': ['A valid integer is required.']})])
776+
777+
For this reason, the nested serialiser approach would be the first to try. You
778+
would use the custom field approach when the nested serialiser becomes infeasible
779+
or overly complex.
780+
781+
649782
# Third party packages
650783

651784
The following third party packages are also available.

0 commit comments

Comments
 (0)