DRF Serializers
Serializers in Django REST Framework (DRF) are powerful tools that allow you to convert complex data types, such as Django model instances, into native Python datatypes that can then be easily rendered into JSON, XML, or other content types. They also provide deserialization, allowing parsed data to be converted back into complex types after validating the incoming data.
Creating a Serializer
Section titled “Creating a Serializer”To create a serializer, you typically define a class that inherits from serializers.Serializer and specify the model you want to serialize along with the fields you want to include. Here’s an example based on the Product model:
# reference models.pyfrom django.db import modelsfrom django.core.validators import MinValueValidator
class Collection(models.Model): title = models.CharField(max_length=255)
def __str__(self) -> str: return self.title
class Product(models.Model): title = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) unit_price = models.DecimalField( max_digits=6, decimal_places=2, validators=[MinValueValidator(1)]) collection = models.ForeignKey( Collection, on_delete=models.CASCADE, related_name='products' )
def __str__(self) -> str: return self.titleThis model represents a product with a title, description, unit price. To create a serializer for this model, you can do the following:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): id = serializers.IntegerField() title = serializers.CharField(max_length=255) unit_price = serializers.DecimalField(max_digits=6, decimal_places=2)Now you see how to use this in a view to serialize data:
from rest_framework.views import APIViewfrom rest_framework.response import Responsefrom .models import Product
@api_view(['GET'])def product_list(request): products = Product.objects.all() serializer = ProductSerializer(products, many=True) return Response(serializer.data)SerializerMethodField
Section titled “SerializerMethodField”SerializerMethodField is a read-only field that gets its value by calling a method on the serializer class. This is useful when you want to include additional data in the serialized output that is not directly available as a model field. For example, price with tax can be calculated from the unit_price field:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): price_with_tax = serializers.SerializerMethodField(method_name='calculate_tax')
def calculate_tax(self, obj): # Assuming a tax rate of 10% return obj.unit_price * 1.1source argument in serializer fields
Section titled “source argument in serializer fields”The source argument in serializer fields allows you to specify the attribute or method on the model that should be used to populate the field. This is particularly useful when you want to include data that is not directly a model field, such as a method that calculates a value or a related object’s attribute. Example: unit_price to price:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): # other fields... price = serializers.DecimalField( max_digits=6, decimal_places=2, source='unit_price' # this is the source )Read-only
Section titled “Read-only”By default, all fields in a serializer are read-write, meaning they can be used for both serialization and deserialization. However, you can make a field read-only by setting the read_only argument to True. This means that the field will be included in the serialized output but will not be used for deserialization when creating or updating instances. For example:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): # other fields... price_with_tax = serializers.SerializerMethodField(read_only=True)Write-only
Section titled “Write-only”Similarly, you can make a field write-only by setting the write_only argument to True. This means that the field will be used for deserialization when creating or updating instances but will not be included in the serialized output. For example:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): # other fields... secret_code = serializers.CharField(max_length=100, write_only=True)Related fields Serializers
Section titled “Related fields Serializers”When you have relationships between models (like foreign keys), DRF provides several ways to represent these relationships in your serializers. The most common related fields are PrimaryKeyRelatedField, StringRelatedField, HyperlinkedRelatedField, and nested serializers.
PrimaryKeyRelatedField
Section titled “PrimaryKeyRelatedField”When you have a foreign key relationship in your model, you can use PrimaryKeyRelatedField to represent the related object by its primary key. For example, if you want to include the collection field in the ProductSerializer, you can do it like this:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): # other fields... collection = serializers.PrimaryKeyRelatedField(queryset=Collection.objects.all())StringRelatedField
Section titled “StringRelatedField”If you want to represent the related object using its string representation (as defined by the __str__ method in the model), you can use StringRelatedField. For example:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): # other fields... collection = serializers.StringRelatedField()HyperlinkedRelatedField
Section titled “HyperlinkedRelatedField”If you want to represent the related object as a hyperlink to its detail view, you can use HyperlinkedRelatedField. This requires that you have a URL pattern defined for the related model. For example:
from rest_framework import serializers
class ProductSerializer(serializers.Serializer): # other fields... collection = serializers.HyperlinkedRelatedField( queryset=Collection.objects.all(), view_name='collection-detail', read_only=True )# views.pyfrom rest_framework.views import APIViewfrom rest_framework.response import Response
@api_view(['GET'])def product_detail(request, pk): product = Product.objects.select_related('collection').get(pk=pk) serializer = ProductSerializer(product, context={'request': request}) return Response(serializer.data)
@api_view(['GET'])def collection_detail(request, pk): collection = get_object_or_404(Collection, pk=pk) serializer = CollectionSerializer(collection) return Response(serializer.data)# urls.pyfrom django.urls import path
urlpatterns = [ path('collections/<int:pk>/', collection_detail, name='collection-detail'),]Nested serializers
Section titled “Nested serializers”Nested serializers allow you to include related objects in the serialized output. For example, if you want to include the details of the related Collection in the ProductSerializer, you can define a nested serializer for Collection and use it in the ProductSerializer:
class CollectionSerializer(serializers.Serializer): id = serializers.IntegerField() title = serializers.CharField(max_length=255)
class ProductSerializer(serializers.Serializer): # other fields... collection = CollectionSerializer()Model Serializer
Section titled “Model Serializer”Model serializers provide a shortcut for creating serializers that deal with model instances and querysets. They automatically generate a set of fields based on the model and can also include simple default implementations for create() and update() methods. Here’s how you can create a model serializer for the Product model:
from rest_framework import serializersfrom .models import Product
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'collection']Overriding Fields
Section titled “Overriding Fields”You can override fields in a ModelSerializer to customize their behavior. For example, in collection field if you want to use hyperlinkedRelatedField instead of the default primary key representation, you can do it like this:
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'collection']
collection = serializers.HyperlinkedRelatedField( queryset=Collection.objects.all(), view_name='collection-detail', read_only=True )In the Model Serializer if we overwrite a field, Then the default model field will be ignored and the overwritten field will be used instead. So in the above example, the collection field in the ProductSerializer will use the HyperlinkedRelatedField instead of the default primary key representation that would have been generated by the ModelSerializer.
Custom Fields
Section titled “Custom Fields”You can also add custom fields to a ModelSerializer that are not directly related to the model fields. For example, if you want to include a price_with_tax field that calculates the price including tax, you can do it like this:
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'price_with_tax', 'collection']
price_with_tax = serializers.SerializerMethodField()
def get_price_with_tax(self, obj): # Assuming a tax rate of 10% return obj.unit_price * Decimal(1.1)__all__ in fields
Section titled “__all__ in fields”If you want to include all fields from the model in your serializer, you can use __all__ in the fields attribute of the Meta class. This will automatically include all fields defined in the model without having to list them individually. For example:
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = '__all__'Annotate Fields Serializer
Section titled “Annotate Fields Serializer”When you use annotate() in a queryset, Django dynamically adds extra fields (like counts, sums, etc.) to each object. However, these fields are not part of the model, so the serializer does not recognize them automatically.
To include an annotated field in a serializer:
- You must explicitly declare the field in the serializer.
- You must also add it to the fields list inside the Meta class.
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Collection fields = ["id", "title", "products_count"]
products_count = serializers.IntegerField()# views.pyfrom django.db.models import Count
@api_view(['GET'])def collection_list(request): collections = Collection.objects.annotate(products_count=Count('products')) serializer = CollectionSerializer(collections, many=True) return Response(serializer.data)Receving Data
Section titled “Receving Data”We can also use serializers to receive and validate incoming data. For example, if you want to create a new product using a POST request, you can do it like this:
from rest_framework import serializersfrom .models import Product
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'collection']# views.pyfrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework import status
@api_view(['POST'])def create_product(request): serializer = ProductSerializer(data=request.data) if serializer.is_valid(): product = serializer.save() return Response( ProductSerializer(product).data, status=status.HTTP_201_CREATED ) else: return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST )Validating Data
Section titled “Validating Data”When you receive data through a serializer, you can validate it using the is_valid() method. This method checks if the data meets the validation criteria defined in the serializer fields. If the data is valid, you can access the validated data through the validated_data property. For example:
# manual error handling@api_view(['POST'])def create_product(request): serializer = ProductSerializer(data=request.data) if serializer.is_valid(): validated_data = serializer.validated_data return Response( ProductSerializer(product).data, status=status.HTTP_201_CREATED ) else: return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST )
# automatic error handling@api_view(['POST'])def create_product(request): serializer = ProductSerializer(data=request.data) # This will raise a ValidationError if the data is invalid serializer.is_valid(raise_exception=True) product = serializer.save() return Response( ProductSerializer(product).data, status=status.HTTP_201_CREATED )Custom Validation
Section titled “Custom Validation”You can also add custom validation logic to your serializers by defining validate_<field_name> methods or by overriding the validate method for object-level validation. For example, if you want to ensure that the unit_price is greater than zero, you can do it like this:
# field-level validationclass ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'collection']
def validate_unit_price(self, value): if value <= 0: raise serializers.ValidationError("Unit price must be greater than zero.") return value
# object-level validationclass ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'collection']
def validate(self, data): if data['unit_price'] <= 0: raise serializers.ValidationError("Unit price must be greater than zero.") return dataSaving Data
Section titled “Saving Data”When you call the save() method on a serializer, it will either create a new instance or update an existing instance based on whether the serializer was initialized with an instance or not. The save() method will call the create() or update() method of the serializer, which you can override to customize the saving behavior. For example:
# views.py@api_view(['POST'])def create_product(request): serializer = ProductSerializer(data=request.data) serializer.is_valid(raise_exception=True) product = serializer.save() return Response( ProductSerializer(product).data, status=status.HTTP_201_CREATED )Updating Data
Section titled “Updating Data”When you want to update an existing instance, you can initialize the serializer with the instance you want to update and the new data. For example:
@api_view(["GET",'PUT'])def update_product(request, pk): product = get_object_or_404(Product, pk=pk)
if request.method == 'GET': serializer = ProductSerializer(product) return Response(serializer.data)
elif request.method == 'PUT': serializer = ProductSerializer(product, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data)Overriding create() and update() methods
Section titled “Overriding create() and update() methods”By default, the ModelSerializer provides implementations for the create() and update() methods that simply create or update model instances based on the validated data. However, you can override these methods to add custom logic during the creation or updating of instances. For example:
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'title', 'unit_price', 'collection']
def create(self, validated_data): # Custom logic for creating a product product = Product(**validated_data) product.field_name = 'custom value' # example of adding custom logic product.save() return product
def update(self, instance, validated_data): # Custom logic for updating a product
# example of adding custom logic instance.title = validated_data.get('title').strip() instance.field_name = 'custom value' instance.save() return instanceDeleteing Data
Section titled “Deleteing Data”To delete an instance using a serializer, you can call the delete() method on the instance you want to delete. For example:
@api_view(['DELETE'])def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) product.delete() return Response(status=status.HTTP_204_NO_CONTENT)Handling Delete Error
Section titled “Handling Delete Error”If you try to delete a object that have models.PROTECT or models.RESTRICT on_delete behavior, it will raise a ProtectedError or RestrictedError respectively. To handle this error, you can catch the exception and return an appropriate response. For example:
from django.db.models import ProtectedErrorfrom rest_framework import status
@api_view(['DELETE'])def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) try: product.delete() return Response(status=status.HTTP_204_NO_CONTENT) except ProtectedError: return Response( data={"detail": "Cannot delete this product because it is protected."}, status=status.HTTP_400_BAD_REQUEST )