3.0 announcement - swinton/django-rest-framework GitHub Wiki
The 3.0 release is now in beta and ready for final testing. You can install the development version directly from GitHub like so:
pip install https://github.com/tomchristie/django-rest-framework/archive/3.0-beta.zip
Currently the only known remaining blockers are documentation issues and tickets. Any critical bugs raised in the next week or two will be resolved for the 3.0 release, but otherwise consider this as code-complete.
Please work through this document throughly in order to understand the API differences that exist between 2.4 and 3.0.
Your feedback on the upgrade process and 3.0 changes is hugely important!
Please do get in touch via twitter, IRC, a GitHub ticket, or the discussion group.
The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
This release is incremental in nature. There are some breaking API changes, and upgrading will require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.
The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier.
Notable features of this new release include:
- Printable representations on serializers that allow you to inspect exactly what fields are present on the instance.
- Simple model serializers that are vastly easier to understand and debug, and that make it easy to switch between the implicit
ModelSerializer
class and the explicitSerializer
class. - A new
BaseSerializer
class, making it easier to write serializers for alternative storage backends, or to completely customize your serialization and validation logic. - A cleaner fields API including new classes such as
ListField
andMultipleChoiceField
. - Super simple default implementations for the generic views.
- Support for overriding how validation errors are handled by your API.
- A metadata API that allows you to customize how
OPTIONS
requests are handled by your API. - A more compact JSON output with unicode style encoding turned on by default.
- Templated based HTML form rendering for serializers. This will be finalized as public API in the upcoming 3.1 release.
Significant new functionality continues to be planned for the 3.1 and 3.2 releases. These releases will correspond to the two Kickstarter stretch goals - "Feature improvements" and "Admin interface". Further 3.x releases will present simple upgrades, without the same level of fundamental API changes necessary for the 3.0 release.
Below is an in-depth guide to the API changes and migration notes for 3.0.
The usage of request.DATA
and request.FILES
is now pending deprecation in favor of a single request.data
attribute that contains all the parsed data.
Having separate 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.
You may now pass all the request data to a serializer class in a single argument:
# Do this...
ExampleSerializer(data=request.data)
Instead of passing the files argument separately:
# Don't do this...
ExampleSerializer(data=request.DATA, files=request.FILES)
The usage of request.QUERY_PARAMS
is now pending deprecation in favor of the lowercased request.query_params
.
Previously the serializers used a two-step object creation, as follows:
- Validating the data would create an object instance. This instance would be available as
serializer.object
. - Calling
serializer.save()
would then save the object instance to the database.
This style is in-line with how the ModelForm
class works in Django, but is problematic for a number of reasons:
- Some data, such as many-to-many relationships, cannot be added to the object instance until after it has been saved. This type of data needed to be hidden in some undocumented state on the object instance, or kept as state on the serializer instance so that it could be used when
.save()
is called. - Instantiating model instances directly means that you cannot use model manager classes for instance creation, e.g.
ExampleModel.objects.create(...)
. Manager classes are an excellent layer at which to enforce business logic and application-level data constraints. - The two step process makes it unclear where to put deserialization logic. For example, should extra attributes such as the current user get added to the instance during object creation or during object save?
We now use single-step object creation, like so:
- Validating the data makes the cleaned data available as
serializer.validated_data
. - Calling
serializer.save()
then saves and returns the new object instance.
The resulting API changes are further detailed below.
The .restore_object()
method is now replaced with two separate methods, .create()
and .update()
.
These methods also replace the optional .save_object()
method, which no longer exists.
When using the .create()
and .update()
methods you should both create and save the object instance. This is in contrast to the previous .restore_object()
behavior that would instantiate the object but not save it.
The following example from the tutorial previously used restore_object()
to handle both creating and updating object instances.
def restore_object(self, attrs, instance=None):
if instance:
# Update existing instance
instance.title = attrs.get('title', instance.title)
instance.code = attrs.get('code', instance.code)
instance.linenos = attrs.get('linenos', instance.linenos)
instance.language = attrs.get('language', instance.language)
instance.style = attrs.get('style', instance.style)
return instance
# Create new instance
return Snippet(**attrs)
This would now be split out into two separate methods.
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.code = validated_data.get('code', instance.code)
instance.linenos = validated_data.get('linenos', instance.linenos)
instance.language = validated_data.get('language', instance.language)
instance.style = validated_data.get('style', instance.style)
instance.save()
return instance
def create(self, validated_data):
return Snippet.objects.create(**validated_data)
Note that these methods should return the newly created object instance.
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.
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 as keyword arguments.
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)
serializer.save(user=request.user) # Include the user when saving.
The .is_valid()
method now takes an optional boolean flag, raise_exception
.
Calling .is_valid(raise_exception=True)
will cause a ValidationError
to be raised if the serializer data contains validation errors. This error will be handled by REST framework's default exception handler, allowing you to remove error response handling from your view code.
The handling and formatting of error responses may be altered globally by using the EXCEPTION_HANDLER
settings key.
This change also means it's now possible to alter the style of error responses used by the built-in generic views, without having to include mixin classes or other overrides.
Previously serializers.ValidationError
error was simply a synonym for django.core.exceptions.ValidationError
. This has now been altered so that it inherits from the standard APIException
base class.
The reason behind this is that Django's ValidationError
class is intended for use with HTML forms and its API makes using it slightly awkward with nested validation errors that can occur in serializers.
For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you are always using the serializers.ValidationError
exception class, and not Django's built-in exception.
We strongly recommend that you use the namespaced import style of import serializers
and not from serializers import ValidationError
in order to avoid any potential confusion.
The validate_<field_name>
method hooks that can be attached to serializer classes change their signature slightly and return type. Previously these would take a dictionary of all incoming data, and a key representing the field name, and would return a dictionary including the validated data for that field:
def validate_score(self, attrs, source):
if attrs[score] % 10 != 0:
raise serializers.ValidationError('This field should be a multiple of ten.')
return attrs
This is now simplified slightly, and the method hooks simply take the value to be validated, and return the validated value.
def validate_score(self, value):
if value % 10 != 0:
raise serializers.ValidationError('This field should be a multiple of ten.')
return value
Any ad-hoc validation that applies to more than one field should go in the .validate(self, attrs)
method as usual.
Because .validate_<field_name>
would previously accept the complete dictionary of attributes, it could be used to validate a field depending on the input in another field. Now if you need to do this you should use .validate()
instead.
You can either return non_field_errors
from the validate method by raising a simple ValidationError
def validate(self, attrs):
# serializer.errors == {'non_field_errors': ['A non field error']}
raise serailizers.ValidationError('A non field error')
Alternatively if you want the errors to be against a specific field, use a dictionary of when instantiating the ValidationError
, like so:
def validate(self, attrs):
# serializer.errors == {'my_field': ['A field error']}
raise serailizers.ValidationError({'my_field': 'A field error'})
This ensures you can still write validation that compares all the input fields, but that marks the error against a particular field.
This change also means that we no longer use the .full_clean()
method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner separation, and ensures that there's no automatic validation behavior on ModelSerializer
classes that can't also be easily replicated on regular Serializer
classes.
It's important to note that this change also means that the model .clean()
method will not be called as part of serializer validation, as it would be if using a ModelForm
. Use the serializer .validate()
method to perform a final validation step on incoming data where required.
REST framework 2.x attempted to automatically support writable nested serialization, but the behavior was complex and non-obvious. Attempting to automatically handle these case is problematic:
- There can be complex dependencies involved in order of saving multiple related model instances.
- It's unclear what behavior the user should expect when related models are passed
None
data. - It's unclear how the user should expect to-many relationships to handle updates, creations and deletions of multiple records.
Using the depth
option on ModelSerializer
will now create read-only nested serializers by default.
If you try to use a writable nested serializer without writing a custom create()
and/or update()
method you'll see an assertion error when you attempt to save the serializer. For example:
>>> class ProfileSerializer(serializers.ModelSerializer):
>>> class Meta:
>>> model = Profile
>>> fields = ('address', 'phone')
>>>
>>> class UserSerializer(serializers.ModelSerializer):
>>> profile = ProfileSerializer()
>>> class Meta:
>>> model = User
>>> fields = ('username', 'email', 'profile')
>>>
>>> data = {
>>> 'username': 'lizzy',
>>> 'email': '[email protected]',
>>> 'profile': {'address': '123 Acacia Avenue', 'phone': '01273 100200'}
>>> }
>>>
>>> serializer = UserSerializer(data=data)
>>> serializer.save()
AssertionError: The `.create()` method does not support nested writable fields by default. Write an explicit `.create()` method for serializer `UserSerializer`, or set `read_only=True` on nested serializer fields.
To use writable nested serialization you'll want to declare a nested field on the serializer class, and write the create()
and/or update()
methods explicitly.
class UserSerializer(serializers.ModelSerializer):
profile = ProfileSerializer()
class Meta:
model = User
fields = ('username', 'email', 'profile')
def create(self, validated_data):
profile_data = validated_data.pop('profile')
user = User.objects.create(**validated_data)
Profile.objects.create(user=user, **profile_data)
return user
The single-step object creation makes this far simpler and more obvious than the previous .restore_object()
behavior.
Serializer instances now support a printable representation that allows you to inspect the fields present on the instance.
For instance, given the following example model:
class LocationRating(models.Model):
location = models.CharField(max_length=100)
rating = models.IntegerField()
created_by = models.ForeignKey(User)
Let's create a simple ModelSerializer
class corresponding to the LocationRating
model.
class LocationRatingSerializer(serializer.ModelSerializer):
class Meta:
model = LocationRating
We can now inspect the serializer representation in the Django shell, using python manage.py shell
...
>>> serializer = LocationRatingSerializer()
>>> print(serializer) # Or use `print serializer` in Python 2.x
LocationRatingSerializer():
id = IntegerField(label='ID', read_only=True)
location = CharField(max_length=100)
rating = IntegerField()
created_by = PrimaryKeyRelatedField(queryset=User.objects.all())
The write_only_fields
option on ModelSerializer
has been moved to PendingDeprecation
and replaced with a more generic extra_kwargs
.
class MySerializer(serializer.ModelSerializer):
class Meta:
model = MyModel
fields = ('id', 'email', 'notes', 'is_admin')
extra_kwargs = {
'is_admin': {'write_only': True}
}
Alternatively, specify the field explicitly on the serializer class:
class MySerializer(serializer.ModelSerializer):
is_admin = serializers.BooleanField(write_only=True)
class Meta:
model = MyModel
fields = ('id', 'email', 'notes', 'is_admin')
The read_only_fields
option remains as a convenient shortcut for the more common case.
The view_name
and lookup_field
options have been moved to PendingDeprecation
. They are no longer required, as you can use the extra_kwargs
argument instead:
class MySerializer(serializer.HyperlinkedModelSerializer):
class Meta:
model = MyModel
fields = ('url', 'email', 'notes', 'is_admin')
extra_kwargs = {
'url': {'lookup_field': 'uuid'}
}
Alternatively, specify the field explicitly on the serializer class:
class MySerializer(serializer.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='mymodel-detail',
lookup_field='uuid'
)
class Meta:
model = MyModel
fields = ('url', 'email', 'notes', 'is_admin')
With ModelSerializer
you can now specify field names in the fields
option that refer to model methods or properties. For example, suppose you have the following model:
class Invitation(models.Model):
created = models.DateTimeField()
to_email = models.EmailField()
message = models.CharField(max_length=1000)
def expiry_date(self):
return self.created + datetime.timedelta(days=30)
You can include expiry_date
as a field option on a ModelSerializer
class.
class InvitationSerializer(serializers.ModelSerializer):
class Meta:
model = Invitation
fields = ('to_email', 'message', 'expiry_date')
These fields will be mapped to serializers.ReadOnlyField()
instances.
>>> serializer = InvitationSerializer()
>>> print repr(serializer)
InvitationSerializer():
to_email = EmailField(max_length=75)
message = CharField(max_length=1000)
expiry_date = ReadOnlyField()
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 separated in the REST framework codebase.
You will typically want to continue to use the existing many=True
flag rather than declaring ListSerializer
classes explicitly, but declaring the classes explicitly can be useful if you need to write custom create
or update
methods for bulk updates, or provide for other custom behavior.
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.
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 primitive 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 methods that can be overridden, 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.
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 primitive 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)
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)
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 alternative storage backends.
The following class is an example of a generic serializer that can handle coercing aribitrary objects into primitive representations.
class ObjectSerializer(serializers.BaseSerializer):
"""
A read-only serializer that coerces arbitrary complex objects
into primitive 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))):
# Primitive 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)
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. -
WritableField
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.
REST framework now has more explicit and clear control over validating empty values for fields.
Previously the meaning of the required=False
keyword argument was underspecified. In practice its use meant that a field could either be not included in the input, or it could be included, but be None
or the empty string.
We now have a better separation, with separate required
, allow_none
and allow_blank
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. ForCharField
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 also available and always implies that the field is not required to be in the input. It is unnecessary to use the required
argument when a default is specified, and doing so will result in an error.
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 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 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 has been added. This field acts like ChoiceField
, but returns a set, which may include none, one or many of the valid choices.
The from_native(self, value)
and to_native(self, data)
method names have been replaced with the more obviously named to_internal_value(self, data)
and to_representation(self, value)
.
The field_from_native()
and field_to_native()
methods are removed. Previously you could use these methods if you wanted to customise the behaviour in a way that did not simply lookup the field value from the object. For example...
def field_to_native(self, obj, field_name):
"""A custom read-only field that returns the class name."""
return obj.__class__.__name__
Now if you need to access the entire object you'll instead need to override one or both of the following:
- Use
get_attribute
to modify the attribute value passed toto_representation()
. - Use
get_value
to modify the data value passedto_internal_value()
.
For example:
def get_attribute(self, obj):
# Pass the entire object through to `to_representation()`,
# instead of the standard attribute lookup.
return obj
def to_representation(self, value):
return value.__class__.__name__
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):
organizations = 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):
organizations = 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 implicit ModelSerializer
classes and explicit Serializer
classes.
class AccountSerializer(serializers.ModelSerializer):
organizations = serializers.SlugRelatedField(
slug_field='name',
queryset=Organization.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
.
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')
I've see several codebases that unnecessarily 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')
REST framework now provides new validators that allow you to ensure field uniqueness, while still using a completely explicit Serializer
class instead of using ModelSerializer
.
The UniqueValidator
should be applied to a serializer field, and takes a single queryset
argument.
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
class OrganizationSerializer(serializers.Serializer):
url = serializers.HyperlinkedIdentityField(view_name='organization_detail')
created = serializers.DateTimeField(read_only=True)
name = serializers.CharField(
max_length=100,
validators=UniqueValidator(queryset=Organization.objects.all())
)
The UniqueTogetherValidator
should be applied to a serializer, and takes a queryset
argument and a fields
argument which should be a list or tuple of field names.
class RaceResultSerializer(serializers.Serializer):
category = serializers.ChoiceField(['5k', '10k'])
position = serializers.IntegerField()
name = serializers.CharField(max_length=100)
default_validators = [UniqueTogetherValidator(
queryset=RaceResult.objects.all(),
fields=('category', 'position')
)]
REST framework also now includes explicit validator classes for validating the unique_for_date
, unique_for_month
, and unique_for_year
model field constraints. These are used internally instead of calling into Model.full_clean()
.
These classes are documented in the Validators section of the documentation.
The view logic for the default method handlers has been significantly simplified, due to the new serializers API.
The pre_save
and post_save
hooks no longer exist, but are replaced with perform_create(self, serializer)
and perform_update(self, serializer)
.
These methods should save the object instance by calling serializer.save()
, adding in any additional arguments as required. They may also perform any custom pre-save or post-save behavior.
For example:
def perform_create(self, serializer):
# Include the owner attribute directly, rather than from request data.
instance = serializer.save(owner=self.request.user)
# Perform a custom post-save action.
send_email(instance.to_email, instance.message)
The pre_delete
and post_delete
hooks no longer exist, and are replaced with .perform_destroy(self, instance)
, which should delete the instance and perform any custom actions.
def perform_destroy(self, instance):
# Perform a custom pre-delete action.
send_deletion_alert(user=instance.created_by, deleted=instance)
# Delete the object instance.
instance.delete()
The .object
and .object_list
attributes are no longer set on the view instance. Treating views as mutable object instances that store state during the processing of the view tends to be poor design, and can lead to obscure flow logic.
I would personally recommend that developers treat view instances as immutable objects in their application code.
Allowing PUT
as create operations is problematic, as it necessarily exposes information about the existence or non-existence of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is necessarily 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 may want to include this AllowPUTAsCreateMixin
class as a mixin to your views.
#### Customizing error responses.
The generic views now raise ValidationFailed
exception 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 customize the style of error responses across your entire API, without having to modify any of the generic views.
Behavior for dealing with OPTIONS
requests was previously built directly into the class based views. This has now been properly separated out into a Metadata API that allows the same pluggable style as other API policies in REST framework.
This makes it far easier to use a different style for OPTIONS
responses throughout your API, and makes it possible to create third-party metadata policies.
## Serializers as HTML forms
REST framework 3.0 includes templated HTML form rendering for serializers.
This API should not yet be considered finalized, and will only be promoted to public API for the 3.1 release.
Significant changes that you do need to be aware of include:
- Nested HTML forms are now supported, for example, a
UserSerializer
with a nestedProfileSerializer
will now render a nestedfieldset
when used in the browsable API. - Nested lists of HTML forms are not yet supported, but are planned for 3.1.
- Because we now use templated HTML form generation, the
widget
option is no longer available for serializer fields. You can instead control the template that is used for a given field, by using thestyle
dictionary.
The style
keyword argument can be used to pass through additional information from a serializer field, to the renderer class. In particular, the HTMLFormRenderer
uses the base_template
key to determine which template to render the field with.
For example, to use a textarea
control instead of the default input
control, you would use the following…
additional_notes = serializers.CharField(
style={'base_template': 'text_area.html'}
)
Similarly, to use a radio button control instead of the default select
control, you would use the following…
color_channel = serializers.ChoiceField(
choices=['red', 'blue', 'green'],
style={'base_template': 'radio.html'}
)
This API should be considered provisional, and there may be minor alterations with the incoming 3.1 release.
There are some improvements in the default style we use in our API responses.
Unicode JSON is now the default. The UnicodeJSONRenderer
class no longer exists, and the UNICODE_JSON
setting has been added. To revert this behavior use the new setting:
REST_FRAMEWORK = {
'UNICODE_JSON': False
}
We now output compact JSON in responses by default. For example, we return:
{"email":"[email protected]","is_admin":true}
Instead of the following:
{"email": "[email protected]", "is_admin": true}
The COMPACT_JSON
setting has been added, and can be used to revert this behavior if needed:
REST_FRAMEWORK = {
'COMPACT_JSON': False
}
The FileField
and ImageField
classes are now represented as URLs by default. You should ensure you set Django's standard MEDIA_URL
setting appropriately, and ensure your application serves the uploaded files.
You can revert this behavior, and display filenames in the representation by using the UPLOADED_FILES_USE_URL
settings key:
REST_FRAMEWORK = {
'UPLOADED_FILES_USE_URL': False
}
You can also modify serializer fields individually, using the use_url
argument:
uploaded_file = serializers.FileField(use_url=False)
Also note that you should pass the request
object to the serializer as context when instantiating it, so that a fully qualified URL can be returned. Returned URLs will then be of the form https://example.com/url_path/filename.txt
. For example:
context = {'request': request}
serializer = ExampleSerializer(instance, context=context)
return Response(serializer.data)
If the request is omitted from the context, the returned URLs will be of the form /url_path/filename.txt
.
The custom X-Throttle-Wait-Second
header has now been dropped in favor of the standard Retry-After
header. You can revert this behavior if needed by writing a custom exception handler for your application.
Date and Time objects are now coerced to strings by default in the serializer output. Previously they were returned as Date
, Time
and DateTime
objects, and later coerced to strings by the renderer.
You can modify this behavior globally by settings the existing DATE_FORMAT
, DATETIME_FORMAT
and TIME_FORMAT
settings keys. Setting these values to None
instead of their default value of 'iso-8859-1'
will result in native objects being returned in serializer data.
REST_FRAMEWORK = {
# Return native `Date` and `Time` objects in `serializer.data`
'DATETIME_FORMAT': None
'DATE_FORMAT': None
'TIME_FORMAT': None
}
You can also modify serializer fields individually, using the date_format
, time_format
and datetime_format
arguments:
# Return `DateTime` instances in `serializer.data`, not strings.
created = serializers.DateTimeField(format=None)
Decimals are now coerced to strings by default in the serializer output. Previously they were returned as Decimal
objects, and later coerced to strings by the renderer.
You can modify this behavior globally by using the COERCE_DECIMAL_TO_STRING
settings key.
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False
}
Or modify it on an individual serializer field, using the coerce_to_string
keyword argument.
# Return `Decimal` instances in `serializer.data`, not strings.
amount = serializers.DecimalField(
max_digits=10,
decimal_places=2,
coerce_to_string=False
)
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.
- The serializer
ChoiceField
does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1. - Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
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:
- Public API for using serializers as HTML forms.
- 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.