Skip to main content

Working with serializers

Serialization is the process of converting complex data types into a format that can be easily transmitted and stored. In this guide, we will show you how to use Django Rest Framework serializers with ShipFast.

Create Django models

For the sake of this example let's use the Customer and Product models inside store package. Those models are related through a foreign key, where a customer can have multiple products associated with them. The HashidAutoField is used for the primary key of both models to provide a unique and secure identifier for the objects.

info

Check out the "How to create a new Django app and model in back-end?" guide if you've missed it.

packages/backend/apps/store/models.py
import hashid_field
from django.db import models


class Customer(models.Model):
id = hashid_field.HashidAutoField(primary_key=True)
email = models.EmailField()


class Product(models.Model):
id = hashid_field.HashidAutoField(primary_key=True)
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=8, decimal_places=2)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='products')
tip

We have also added a related_name attribute to the foreign key field. This allows us to access the related products of a customer by calling "customer.products" instead of "product_set" which is the default related name.

Create a Django Rest Framework serializer

info

If you're not familiar with Django Rest Framework serializers here's the official documentation.

In this example, we'll create a serializer for the "Product" model:

packages/backend/apps/store/serializers.py
from hashid_field import rest as hidrest
from rest_framework import serializers
from .models import Product, Customer


class ProductSerializer(serializers.ModelSerializer):
id = hidrest.HashidSerializerCharField(source_field="store.Product.id", read_only=True)
customer = serializers.PrimaryKeyRelatedField(
queryset=Customer.objects.all(),
pk_field=rest.HashidSerializerCharField(),
write_only=True,
)

class Meta:
model = Product
fields = ('id', 'name', 'price', 'customer')
info

The id is a custom serializer field used for hashing the ID of a model. Here's a breakdown of the key components of this field:

  • HashidSerializerCharField: This is a custom serializer field provided by the hashid_field package that hashes an integer ID field using the Hashids algorithm and returns the hashed value as a string.
  • source_field="store.Product.id": This attribute specifies the name of the source field that the serializer field should use to hash the ID. In this case, it's set to "store.Product.id", which means that the serializer field should use the id field of the Product model.

customer is a field that is used to relate the instance being serialized to a Customer instance in the database. PrimaryKeyRelatedField is a serializer field that represents the target of the relationship as a primary key. The queryset parameter specifies the set of objects that can be selected. pk_field specifies the serializer field to use when serializing the primary key value. In this case, HashidSerializerCharField is used to serialize the primary key as a Hashid string.

Data validation

info

More detailed overview on validation can be found in Django Rest Framework Validation section.

Field validators

Field validators are functions that take a value, perform some checks on it, and return the value if it's valid or raise a serializers.ValidationError if it's not. You can write a custom validation method for the name field in the ProductSerializer to disallow special characters. Here's an example implementation:

packages/backend/apps/store/serializers.py
from hashid_field import rest as hidrest
from rest_framework import serializers
from .models import Product, Customer


class ProductSerializer(serializers.ModelSerializer):
id = hidrest.HashidSerializerCharField(source_field="store.Product.id", read_only=True)
customer = serializers.PrimaryKeyRelatedField(
queryset=Customer.objects.all(),
pk_field=rest.HashidSerializerCharField(),
write_only=True,
)

class Meta:
model = Product
fields = ('id', 'name', 'price', 'customer')

def validate_name(self, value):
"""
Check that the name field does not contain special characters.
"""
special_chars = "!@#$%^&*()-+?_=,<>/"
if any(char in special_chars for char in value):
raise serializers.ValidationError("Product name cannot contain special characters.")

return value

In the above code, we have added a validate_name method to the ProductSerializer to validate the name field. The method checks if the value parameter contains any special characters and raises a serializers.ValidationError if it does.

Object validation

The validate method in DRF serializers allows you to perform custom validations on multiple fields at once. It provides an opportunity to perform more complex validation logic that involves multiple fields or attributes. Here's an example implementation on how to override the validate method of the ProductSerializer to validate both the name and price fields. The method checks if the price field is greater than zero and raises a serializers.ValidationError if it's not. It also checks if the name field only contains letters and numbers

danger

The validate method is called after all the individual field-level validations have been performed.

packages/backend/apps/store/serializers.py
from hashid_field import rest as hidrest
from rest_framework import serializers
from .models import Product, Customer


class ProductSerializer(serializers.ModelSerializer):
id = hidrest.HashidSerializerCharField(source_field="store.Product.id", read_only=True)
customer = serializers.PrimaryKeyRelatedField(
queryset=Customer.objects.all(),
pk_field=rest.HashidSerializerCharField(),
write_only=True,
)

class Meta:
model = Product
fields = ('id', 'name', 'price', 'customer')

def validate(self, data):
"""
Check that the price is greater than zero and the name only contains letters and numbers.
"""
if data['price'] <= 0:
raise serializers.ValidationError("Price must be greater than zero.", code="invalid_price")

if not data['name'].isalnum():
raise serializers.ValidationError("Product name can only contain letters and numbers.", code="invalid_product_name")

return data

Returning errors to frontend

Field validators

In a GraphQL API, if a validation error is raised inside a field validator, the response will include an error message formatted like this:

{
"message": "GraphQlValidationError",
"locations": [{"line": 3, "column": 11}],
"path": ["createProduct"],
"extensions": {
"name": [{"message": "Product name cannot contain special characters.", "code": "invalid"}]
}
}
info

See the "Errors format: Field validators" for a detailed description.

Object validation

When it comes to errors raised inside the validate method, the error message will look like below:

{
"message": "GraphQlValidationError",
"locations": [{"line": 3, "column": 11}],
"path": ["createProduct"],
"extensions": {
"non_field_errors": [
{"message": "Price must be greater than zero.", "code": "invalid_price"},
{"message": "Product name can only contain letters and numbers.", "code": "invalid_product_name"}
]
}
}
info

See the "Errors format: Object validation" for a detailed description.