django-rest-framework/docs/topics/3.0-announcement.md
2014-09-26 17:07:47 +01:00

30 KiB

Pre-release notes:

The 3.0 release is now ready for some tentative testing and upgrades for super keen early adopters. You can install the development version directly from GitHub like so:

pip install https://github.com/tomchristie/django-rest-framework/archive/version-3.0.zip

See the Version 3.0 GitHub issue for more details on remaining work.

The most notable outstanding issues still to resolved on the version-3.0 branch are as follows:

  • Forms support for serializers and in the browsable API.
  • Enforcing uniqueness on unique=True and unique_together fields.
  • Optimisations for serialializing primary keys.
  • Refine style of validation errors in some cases, such as validation errors in ListField.
  • .validate() method on fields.
  • .transform_<field>() method on serializers.

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.


REST framework 3.0

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.

New features

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 explicit Serializer 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 plus new ListField and MultipleChoiceField classes.
  • 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.

Below is an in-depth guide to the API changes and migration notes for 3.0.


Request objects

The .data and .query_params properties.

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.

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.

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.

Previously the serializers used a two-step object creation, as follows:

  1. Validating the data would create an object instance. This instance would be available as serializer.object.
  2. 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 save. This type of data needed to be hidden in some undocumentated 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, eg 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:

  1. Validating the data makes the cleaned data available as serializer.validated_data.
  2. Calling serializer.save() then saves and returns the new object instance.

The resulting API changes are further detailed below.

The .create() and .update() methods.

The .restore_object() method is now replaced with two seperate methods, .create() and .update().

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 seperate methods.

def update(self, instance, validated_attrs)
    instance.title = validated_attrs.get('title', instance.title)
    instance.code = validated_attrs.get('code', instance.code)
    instance.linenos = validated_attrs.get('linenos', instance.linenos)
    instance.language = validated_attrs.get('language', instance.language)
    instance.style = validated_attrs.get('style', instance.style)
    instance.save()

def create(self, validated_attrs):
    return Snippet.objects.create(**validated_attrs)

Note that the .create method should return the newly created object instance.

Use .validated_data instead of .object.

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 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)

Printable serializer reprensentations.

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())

Always use fields, not exclude.

The exclude option on ModelSerializer is no longer available. You should use the more explicit fields option instead.

The extra_kwargs option.

The read_only_fields and write_only_fields options on ModelSerializer have been removed 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': {'read_only': True}
        }

Alternatively, specify the field explicitly on the serializer class:

class MySerializer(serializer.ModelSerializer):
    is_admin = serializers.BooleanField(read_only=True)

    class Meta:
        model = MyModel
        fields = ('id', 'email', 'notes', 'is_admin')

Changes to HyperlinkedModelSerializer.

The view_name and lookup_field options have been removed. 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')

Fields for model methods and properties.

With ModelSerilizer 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.

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.

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.

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.

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.

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.

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).

The field_from_native() and field_to_native() methods are removed.

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.

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.

The view logic for the default method handlers has been significantly simplified, due to the new serializers API.

Removal of pre/post save hooks.

The following method hooks no longer exist on the new, simplified, generic views: pre_save, post_save, pre_delete, post_delete.

If you do need custom behavior, you might choose to instead override the .save() method on your serializer class. For example:

def save(self, *args, **kwargs):
    instance = super(MySerializer).save(*args, **kwarg)
    send_email(instance.to_email, instance.message)
	return instance

Alternatively write your view logic exlpicitly, or tie your pre/post save behavior into the model class or model manager.

Removal of view attributes.

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.

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

Behavior for dealing with OPTIONS requests was previously built directly into the class based views. This has now been properly seperated 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.

API style

There are some improvements in the default style we use in our API responses.

Unicode JSON by default.

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
}

Compact JSON by default.

We now output compact JSON in responses by default. For example, we return:

{"email":"amy@example.com","is_admin":true}

Instead of the following:

{"email": "amy@example.com", "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
}

Throttle headers using Retry-After.

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 as ISO-8859-1 strings in serializer data.

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 as strings in serializer data.

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 corece_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.

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.