DRF Serializers
Serializers in Django REST Framework (DRF) are powerful tools that let you turn complex data, like Django model instances, into simple Python data that can then easily be turned into JSON, XML, or other formats. They also work the other way around (this is called deserialization), turning incoming data back into proper Python objects after checking that the data is valid.
Creating a Serializer
Section titled “Creating a Serializer”To create a serializer, you usually define a class that inherits from serializers.Serializer, and list the fields you want to include from the model you are working with. Here is 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, and 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 let’s see how to use this serializer inside a view:
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 you write inside the serializer class. This is handy when you want to add extra data to the serialized output that is not a direct model field. For example, the price including 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 a serializer field lets you point to a different attribute or method on the model to fill in that field’s value. This is useful when you want to show data that is not a direct model field, such as a calculated value or a value from a related object. Example: showing unit_price as 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, every field in a serializer works both ways, meaning it can be used for both serialization and deserialization. But you can make a field read-only by setting read_only=True. This means the field will show up in the serialized output, but it will not be used when creating or updating an instance. 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”In the same way, you can make a field write-only by setting write_only=True. This means the field will be used when creating or updating an instance, but it will not show up 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 your models have relationships between them (like foreign keys), DRF gives you several ways to show these relationships inside your serializers. The most common ones 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 show the related object using just its primary key (its ID). For example, if you want to include the collection field inside 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 show the related object using its string form (the one defined by the __str__ method on 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 show the related object as a clickable link to its detail page, you can use HyperlinkedRelatedField. This needs a URL pattern already set up 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 let you include the full details of a related object inside the serialized output, instead of just an ID or a link. For example, if you want to include the full details of the related Collection inside ProductSerializer, you can define a nested serializer for Collection and use it inside 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 give you a shortcut for building serializers that work with model instances and querysets. They automatically create the fields for you based on the model, and can also include simple default versions of the create() and update() methods. Here is 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 inside a ModelSerializer to change how they behave. For example, if you want the collection field to use HyperlinkedRelatedField instead of the default primary key form, 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 a Model Serializer, if you override a field, the default version of that field gets ignored, and your overridden field is used instead. So in the example above, the collection field in ProductSerializer will use HyperlinkedRelatedField instead of the default primary key form that ModelSerializer would normally create.
Custom Fields
Section titled “Custom Fields”You can also add custom fields to a ModelSerializer that are not directly tied to a model field. For example, if you want to add 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 every field from the model in your serializer, you can use __all__ in the fields attribute inside the Meta class. This automatically includes every field from the model without you having to list them one by one. For example:
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = '__all__'Annotate Fields Serializer
Section titled “Annotate Fields Serializer”When you use annotate() on a queryset, Django adds extra fields (like counts or sums) to each object on the fly. But since these fields are not part of the actual model, the serializer does not pick them up automatically.
To include an annotated field in a serializer:
- You need to declare the field directly in the serializer.
- You also need to add it to the
fieldslist inside theMetaclass.
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)Receiving Data
Section titled “Receiving Data”You can also use serializers to receive and check incoming data. For example, if you want to create a new product through 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 check it using the is_valid() method. This method checks if the data matches the rules set on the serializer’s fields. If the data is valid, you can get the cleaned-up 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 your own validation rules to a serializer by writing validate_<field_name> methods, or by overriding the validate method for checks that involve the whole object. For example, if you want to make sure 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 one, depending on whether the serializer was given an existing instance when it was created. The save() method internally calls either create() or update() on the serializer, and you can override these methods to customize how saving works. 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 create the serializer with that instance plus 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, ModelSerializer already comes with create() and update() methods that simply create or update a model instance using the validated data. But you can override these methods to add your own custom logic while creating or updating 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 instanceDeleting Data
Section titled “Deleting Data”To delete an instance, you call the delete() method directly on the instance you want to remove. 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 an object that has models.PROTECT or models.RESTRICT set as its on_delete behavior, it will raise a ProtectedError or RestrictedError. To handle this, you can catch the exception and return a proper 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 )