diff options
Diffstat (limited to 'docs/topics/3.0-announcement.md')
| -rw-r--r-- | docs/topics/3.0-announcement.md | 344 |
1 files changed, 323 insertions, 21 deletions
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index cd883cdd..1795611c 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -4,36 +4,65 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr # REST framework 3.0 -**Note incremental nature, discuss upgrading.** +**TODO**: Note incremental nature, discuss upgrading, motivation, features. -## Motivation - -**TODO** +* Serializer reprs. +* Non-magical model serializers. +* Base serializer class. +* Clean logic in views, serializers, fields. --- ## Request objects -#### The `request.data` property. +#### The `.data` and `.query_params` properties. -**TODO** +The usage of `request.DATA` and `request.FILES` is now discouraged in favor of a single `request.data` attribute that contains *all* the parsed data. -#### The parser API. +Having seperate attributes is reasonable for web applications that only ever parse URL encoded or MultiPart requests, but makes less sense for the general-purpose request parsing that REST framework supports. -**TODO** +You may now pass all the request data to a serializer class in a single argument: + + ExampleSerializer(data=request.data) + +Instead of passing the files argument seperately: + + # Don't do this... + ExampleSerializer(data=request.DATA, files=request.FILES) + + +The usage of `request.QUERY_PARAMS` is now discouraged in favor of the lowercased `request.query_params`. ## Serializers #### Single-step object creation. +#### The `.create()` and `.update()` methods. + **TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance. -**TODO**: Drop`.object`, use `.validated_data` or get the instance with `.save()`. +#### Use `.validated_data` instead of `.object`. -#### The `BaseSerializer` class. +You must now use the `.validated_data` attribute if you need to inspect the data before saving, rather than using the `.object` attribute, which no longer exists. -**TODO** +For example the following code *is no longer valid*: + + if serializer.is_valid(): + name = serializer.object.name # Inspect validated field data. + logging.info('Creating ticket "%s"' % name) + serializer.object.user = request.user # Include the user when saving. + serializer.save() +Instead of using `.object` to inspect a partially constructed instance, you would now use `.validated_data` to inspect the cleaned incoming values. Also you can't set extra attributes on the instance directly, but instead pass them to the `.save()` method using the `extras` keyword argument. + +The corresponding code would now look like this: + + if serializer.is_valid(): + name = serializer.validated_data['name'] # Inspect validated field data. + logging.info('Creating ticket "%s"' % name) + extras = {'user': request.user} # Include the user when saving. + serializer.save(extras=extras) + #### Always use `fields`, not `exclude`. The `exclude` option is no longer available. You should use the more explicit `fields` option instead. @@ -111,42 +140,287 @@ These fields will be mapped to `serializers.ReadOnlyField()` instances. message = CharField(max_length=1000) expiry_date = ReadOnlyField() +#### The `ListSerializer` class. + +The `ListSerializer` class has now been added, and allows you to create base serializer classes for only accepting multiple inputs. + + class MultipleUserSerializer(ListSerializer): + child = UserSerializer() + +You can also still use the `many=True` argument to serializer classes. It's worth noting that `many=True` argument transparently creates a `ListSerializer` instance, allowing the validation logic for list and non-list data to be cleanly seperated in the REST framework codebase. + +See also the new `ListField` class, which validates input in the same way, but does not include the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on. + +#### The `BaseSerializer` class. + +REST framework now includes a simple `BaseSerializer` class that can be used to easily support alternative serialization and deserialization styles. + +This class implements the same basic API as the `Serializer` class: + +* `.data` - Returns the outgoing primative representation. +* `.is_valid()` - Deserializes and validates incoming data. +* `.validated_data` - Returns the validated incoming data. +* `.errors` - Returns an errors during validation. +* `.save()` - Persists the validated data into an object instance. + +There are four mathods that can be overriding, depending on what functionality you want the serializer class to support: + +* `.to_representation()` - Override this to support serialization, for read operations. +* `.to_internal_value()` - Override this to support deserialization, for write operations. +* `.create()` and `.update()` - Overide either or both of these to support saving instances. + +##### Read-only serializers. + +To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model: + + class HighScore(models.Model): + created = models.DateTimeField(auto_now_add=True) + player_name = models.CharField(max_length=10) + score = models.IntegerField() + +It's simple to create a read-only serializer for converting `HighScore` instances into primative data types. + + class HighScoreSerializer(serializers.BaseSerializer): + def to_representation(self, obj): + return { + 'score': obj.score, + 'player_name': obj.player_name + } + +We can now use this class to serialize single `HighScore` instances: + + @api_view(['GET']) + def high_score(request, pk): + instance = HighScore.objects.get(pk=pk) + serializer = HighScoreSerializer(instance) + return Response(serializer.data) + +Or use it to serialize multiple instances: + + @api_view(['GET']) + def all_high_scores(request): + queryset = HighScore.objects.order_by('-score') + serializer = HighScoreSerializer(queryset, many=True) + return Response(serializer.data) + +##### Read-write serializers. + +To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `ValidationError` if the supplied data is in an incorrect format. + +Once you've implemented `.to_internal_value()`, the basic validation API will be available on the serializer, and you will be able to use `.is_valid()`, `.validated_data` and `.errors`. + +If you want to also support `.save()` you'll need to also implement either or both of the `.create()` and `.update()` methods. + +Here's a complete example of our previous `HighScoreSerializer`, that's been updated to support both read and write operations. + + class HighScoreSerializer(serializers.BaseSerializer): + def to_internal_value(self, data): + score = data.get('score') + player_name = data.get('player_name') + + # Perform the data validation. + if not score: + raise ValidationError({ + 'score': 'This field is required.' + }) + if not player_name: + raise ValidationError({ + 'player_name': 'This field is required.' + }) + if len(player_name) > 10: + raise ValidationError({ + 'player_name': 'May not be more than 10 characters.' + }) + + # Return the validated values. This will be available as + # the `.validated_data` property. + return { + 'score': int(score), + 'player_name': player_name + } + + def to_representation(self, obj): + return { + 'score': obj.score, + 'player_name': obj.player_name + } + + def create(self, validated_data): + return HighScore.objects.create(**validated_data) + +#### Creating new base classes with `BaseSerializer`. + +The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles or for integrating with different storage backends. + +The following class is an example of a generic serializer that can handle coercing aribitrary objects into primative representations. + + class ObjectSerializer(serializers.BaseSerializer): + """ + A read-only serializer that coerces arbitrary complex objects + into primative representations. + """ + def to_representation(self, obj): + for attribute_name in dir(obj): + attribute = getattr(obj, attribute_name) + if attribute_name('_'): + # Ignore private attributes. + pass + elif hasattr(attribute, '__call__'): + # Ignore methods and other callables. + pass + elif isinstance(attribute, (str, int, bool, float, type(None))): + # Primative types can be passed through unmodified. + output[attribute_name] = attribute + elif isinstance(attribute, list): + # Recursivly deal with items in lists. + output[attribute_name] = [ + self.to_representation(item) for item in attribute + ] + elif isinstance(attribute, dict): + # Recursivly deal with items in dictionarys. + output[attribute_name] = { + str(key): self.to_representation(value) + for key, value in attribute.items() + } + else: + # Force anything else to its string representation. + output[attribute_name] = str(attribute) ## Serializer fields #### The `Field` and `ReadOnly` field classes. -**TODO** +There are some minor tweaks to the field base classes. + +Previously we had these two base classes: + +* `Field` as the base class for read-only fields. A default implementation was included for serializing data. +* `WriteableField` as the base class for read-write fields. + +We now use the following: + +* `Field` is the base class for all fields. It does not include any default implementation for either serializing or deserializing data. +* `ReadOnlyField` is a concrete implementation for read-only fields that simply returns the attribute value without modification. + +#### The `required`, `allow_none`, `allow_blank` and `default` arguments. + +REST framework now has more explict and clear control over validating empty values for fields. + +Previously the meaning of the `required=False` keyword argument was underspecified. In practice it's use meant that a field could either be not included in the input, or it could be included, but be `None`. + +We now have a better seperation, with seperate `required` and `allow_none` arguments. + +The following set of arguments are used to control validation of empty values: + +* `required=False`: The value does not need to be present in the input, and will not be passed to `.create()` or `.update()` if it is not seen. +* `default=<value>`: The value does not need to be present in the input, and a default value will be passed to `.create()` or `.update()` if it is not seen. +* `allow_none=True`: `None` is a valid input. +* `allow_blank=True`: `''` is valid input. For `CharField` and subclasses only. + +Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_none=True` or `allow_blank=True` if required. + +The `default` argument is there if you need it, but you'll more typically want defaults to be set on model fields, rather than serializer fields. #### Coercing output types. -**TODO** +The previous field implementations did not forcibly coerce returned values into the correct type in many cases. For example, an `IntegerField` would return a string output if the attribute value was a string. We now more strictly coerce to the correct return type, leading to more constrained and expected behavior. -#### The `ListSerializer` class. +#### The `ListField` class. -**TODO** +The `ListField` class has now been added. This field validates list input. It takes a `child` keyword argument which is used to specify the field used to validate each item in the list. For example: + + scores = ListField(child=IntegerField(min_value=0, max_value=100)) + +You can also use a declarative style to create new subclasses of `ListField`, like this: + + class ScoresField(ListField): + child = IntegerField(min_value=0, max_value=100) + +We can now use the `ScoresField` class inside another serializer: + + scores = ScoresField() + +See also the new `ListSerializer` class, which validates input in the same way, but also includes the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on. + +#### The `ChoiceField` class may now accept a flat list. + +The `ChoiceField` class may now accept a list of choices in addition to the existing style of using a list of pairs of `(name, display_value)`. The following is now valid: + + color = ChoiceField(choices=['red', 'green', 'blue']) #### The `MultipleChoiceField` class. -**TODO** +The `MultipleChoiceField` class has been added. This field acts like `ChoiceField`, but returns a set, which may include none, one or many of the valid choices. #### Changes to the custom field API. -**TODO** `to_representation`, `to_internal_value`. +The `from_native(self, value)` and `to_native(self, data)` method names have been replaced with the more obviously named `to_representation(self, value)` and `to_internal_value(self, data)`. -#### Explicit `querysets` required on relational fields. +The `field_from_native()` and `field_to_native()` methods are removed. -**TODO** +#### Explicit `queryset` required on relational fields. + +Previously relational fields that were explicitly declared on a serializer class could omit the queryset argument if (and only if) they were declared on a `ModelSerializer`. + +This code *would be valid* in `2.4.3`: + + class AccountSerializer(serializers.ModelSerializer): + organisations = serializers.SlugRelatedField(slug_field='name') + + class Meta: + model = Account + +However this code *would not be valid* in `2.4.3`: + + # Missing `queryset` + class AccountSerializer(serializers.Serializer): + organisations = serializers.SlugRelatedField(slug_field='name') + + def restore_object(self, attrs, instance=None): + # ... + +The queryset argument is now always required for writable relational fields. +This removes some magic and makes it easier and more obvious to move between implict `ModelSerializer` classes and explicit `Serializer` classes. + + class AccountSerializer(serializers.ModelSerializer): + organisations = serializers.SlugRelatedField( + slug_field='name', + queryset=Organisation.objects.all() + ) + + class Meta: + model = Account + +The `queryset` argument is only ever required for writable fields, and is not required or valid for fields with `read_only=True`. #### Optional argument to `SerializerMethodField`. -**TODO** +The argument to `SerializerMethodField` is now optional, and defaults to `get_<field_name>`. For example the following is valid: + + class AccountSerializer(serializers.Serializer): + # `method_name='get_billing_details'` by default. + billing_details = serializers.SerializerMethodField() + + def get_billing_details(self, account): + return calculate_billing(account) + +In order to ensure a consistent code style an assertion error will be raised if you include a redundant method name argument that matches the default method name. For example, the following code *will raise an error*: + + billing_details = serializers.SerializerMethodField('get_billing_details') + +#### Enforcing consistent `source` usage. + +I've see several codebases that unneccessarily include the `source` argument, setting it to the same value as the field name. This usage is redundant and confusing, making it less obvious that `source` is usually not required. + +The following usage will *now raise an error*: + + email = serializers.EmailField(source='email') ## Generic views #### Simplification of view logic. -**TODO** +The view logic for the default method handlers has been significantly simplified, due to the new serializers API. #### Removal of pre/post save hooks. @@ -169,6 +443,20 @@ I would personally recommend that developers treat view instances as immutable o #### PUT as create. +Allowing `PUT` as create operations is problematic, as it neccessarily exposes information about the existence or non-existance of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is neccessarily a better default behavior than simply returning `404` responses. + +Both styles "`PUT` as 404" and "`PUT` as create" can be valid in different circumstances, but we've now opted for the 404 behavior as the default, due to it being simpler and more obvious. + +If you need to restore the previous behavior you can include the `AllowPUTAsCreateMixin` class in your view. This class can be imported from `rest_framework.mixins`. + +#### Customizing error responses. + +The generic views now raise `ValidationError` for invalid data. This exception is then dealt with by the exception handler, rather than the view returning a `400 Bad Request` response directly. + +This change means that you can now easily cusomize the style of error responses across your entire API, without having to modify any of the generic views. + +## The metadata API + **TODO** ## API style @@ -241,3 +529,17 @@ Or modify it on an individual serializer field, using the `corece_to_string` key ) The default JSON renderer will return float objects for uncoerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs. + +## What's coming next. + +3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes. + +The 3.1 release is planned to address improvements in the following components: + +* Request parsing, mediatypes & the implementation of the browsable API. +* Introduction of a new pagination API. +* Better support for API versioning. + +The 3.2 release is planned to introduce an alternative admin-style interface to the browsable API. + +You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones). |
