Agent skill

drf-serializer

Generate Django REST Framework serializers with validation, nested relationships, and best practices. Use when creating or updating API serializers, especially for complex models with relationships.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/drf-serializer

SKILL.md

You are a Django REST Framework serializer expert. You generate clean, efficient, and well-validated serializers for REST APIs.

Serializer Generation Capabilities

1. Basic ModelSerializer

Generate serializers from models with appropriate fields and validation.

Input: Model information Output: Complete serializer with:

  • Appropriate fields
  • Read-only fields
  • Validation methods
  • Meta configuration

Example:

python
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    """Serializer for Product model"""

    class Meta:
        model = Product
        fields = [
            'id',
            'name',
            'slug',
            'description',
            'price',
            'stock',
            'category',
            'created_at',
            'updated_at',
        ]
        read_only_fields = ['id', 'slug', 'created_at', 'updated_at']

    def validate_price(self, value):
        """Ensure price is positive"""
        if value <= 0:
            raise serializers.ValidationError("Price must be greater than zero")
        return value

    def validate_stock(self, value):
        """Ensure stock is non-negative"""
        if value < 0:
            raise serializers.ValidationError("Stock cannot be negative")
        return value

2. Nested Serializers

Handle relationships with nested serialization for both read and write operations.

Pattern 1: Read-only nested

python
class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name', 'slug']


class ProductSerializer(serializers.ModelSerializer):
    category = CategorySerializer(read_only=True)  # Nested for reading
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(),
        source='category',
        write_only=True  # PK for writing
    )

    class Meta:
        model = Product
        fields = [
            'id',
            'name',
            'price',
            'category',      # Nested object in response
            'category_id',   # ID for creating/updating
        ]

Pattern 2: Writable nested

python
class AddressSerializer(serializers.ModelSerializer):
    class Meta:
        model = Address
        fields = ['street', 'city', 'state', 'zip_code']


class CustomerSerializer(serializers.ModelSerializer):
    addresses = AddressSerializer(many=True)

    class Meta:
        model = Customer
        fields = ['id', 'name', 'email', 'addresses']

    def create(self, validated_data):
        """Handle nested address creation"""
        addresses_data = validated_data.pop('addresses')
        customer = Customer.objects.create(**validated_data)

        for address_data in addresses_data:
            Address.objects.create(customer=customer, **address_data)

        return customer

    def update(self, instance, validated_data):
        """Handle nested address updates"""
        addresses_data = validated_data.pop('addresses', None)

        # Update customer fields
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()

        # Update addresses if provided
        if addresses_data is not None:
            instance.addresses.all().delete()  # Clear existing
            for address_data in addresses_data:
                Address.objects.create(customer=instance, **address_data)

        return instance

3. Different Serializers for Different Operations

List Serializer (lightweight):

python
class ProductListSerializer(serializers.ModelSerializer):
    """Lightweight serializer for list view"""
    category_name = serializers.CharField(source='category.name', read_only=True)
    thumbnail = serializers.SerializerMethodField()

    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'category_name', 'thumbnail']

    def get_thumbnail(self, obj):
        """Get thumbnail URL"""
        if obj.image:
            return obj.image.url
        return None

Detail Serializer (comprehensive):

python
class ProductDetailSerializer(serializers.ModelSerializer):
    """Detailed serializer for detail view"""
    category = CategorySerializer(read_only=True)
    reviews = ReviewSerializer(many=True, read_only=True)
    average_rating = serializers.SerializerMethodField()
    related_products = serializers.SerializerMethodField()

    class Meta:
        model = Product
        fields = [
            'id',
            'name',
            'slug',
            'description',
            'price',
            'stock',
            'category',
            'images',
            'reviews',
            'average_rating',
            'related_products',
            'created_at',
            'updated_at',
        ]

    def get_average_rating(self, obj):
        """Calculate average rating"""
        from django.db.models import Avg
        result = obj.reviews.aggregate(avg_rating=Avg('rating'))
        return result['avg_rating']

    def get_related_products(self, obj):
        """Get related products"""
        related = Product.objects.filter(
            category=obj.category
        ).exclude(id=obj.id)[:4]

        return ProductListSerializer(related, many=True, context=self.context).data

Create/Update Serializer:

python
class ProductWriteSerializer(serializers.ModelSerializer):
    """Serializer for creating/updating products"""

    class Meta:
        model = Product
        fields = [
            'name',
            'description',
            'price',
            'stock',
            'category',
            'featured',
        ]

    def validate(self, attrs):
        """Cross-field validation"""
        if attrs.get('featured') and attrs.get('stock', 0) == 0:
            raise serializers.ValidationError(
                "Featured products must have stock available"
            )
        return attrs

4. SerializerMethodField Patterns

Add computed or derived fields:

python
class OrderSerializer(serializers.ModelSerializer):
    total_items = serializers.SerializerMethodField()
    discount_amount = serializers.SerializerMethodField()
    final_total = serializers.SerializerMethodField()
    status_display = serializers.CharField(source='get_status_display', read_only=True)

    class Meta:
        model = Order
        fields = [
            'id',
            'subtotal',
            'tax',
            'total',
            'total_items',
            'discount_amount',
            'final_total',
            'status',
            'status_display',
        ]

    def get_total_items(self, obj):
        """Count total items in order"""
        return obj.items.count()

    def get_discount_amount(self, obj):
        """Calculate discount amount"""
        if obj.discount_code:
            return obj.subtotal * (obj.discount_code.percentage / 100)
        return 0

    def get_final_total(self, obj):
        """Calculate final total with discounts"""
        discount = self.get_discount_amount(obj)
        return obj.total - discount

5. Dynamic Fields

Allow clients to specify which fields to include:

python
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument to
    control which fields should be displayed.
    """

    def __init__(self, *args, **kwargs):
        # Get fields from context or kwargs
        fields = kwargs.pop('fields', None)
        context = kwargs.get('context', {})
        request = context.get('request')

        if request and not fields:
            # Get fields from query param: ?fields=id,name,price
            fields = request.query_params.get('fields')

        super().__init__(*args, **kwargs)

        if fields:
            fields = fields.split(',') if isinstance(fields, str) else fields
            # Drop fields not specified
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)


class ProductSerializer(DynamicFieldsModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'description', 'price', 'stock', 'category']

# Usage: GET /api/products/?fields=id,name,price

6. Validation Patterns

Field-level validation:

python
def validate_email(self, value):
    """Validate email is unique"""
    if User.objects.filter(email=value).exists():
        raise serializers.ValidationError("Email already in use")
    return value.lower()

def validate_phone(self, value):
    """Validate phone number format"""
    import re
    phone_regex = r'^\+?1?\d{9,15}$'
    if not re.match(phone_regex, value):
        raise serializers.ValidationError("Invalid phone number format")
    return value

Object-level validation:

python
def validate(self, attrs):
    """Validate entire object"""
    start_date = attrs.get('start_date')
    end_date = attrs.get('end_date')

    if start_date and end_date and start_date > end_date:
        raise serializers.ValidationError({
            'end_date': "End date must be after start date"
        })

    # Check business rules
    if attrs.get('discount') > 0 and not attrs.get('discount_code'):
        raise serializers.ValidationError(
            "Discount code required when discount is applied"
        )

    return attrs

Custom validators:

python
from rest_framework.validators import UniqueTogetherValidator

class BookingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Booking
        fields = ['id', 'room', 'date', 'time_slot', 'user']
        validators = [
            UniqueTogetherValidator(
                queryset=Booking.objects.all(),
                fields=['room', 'date', 'time_slot'],
                message="This time slot is already booked"
            )
        ]

7. Hyperlinked Serializers

Use hyperlinks instead of primary keys:

python
class ProductHyperlinkedSerializer(serializers.HyperlinkedModelSerializer):
    category = serializers.HyperlinkedRelatedField(
        view_name='category-detail',
        read_only=True
    )
    reviews = serializers.HyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='review-detail'
    )

    class Meta:
        model = Product
        fields = [
            'url',  # Self link
            'id',
            'name',
            'category',
            'reviews',
        ]
        extra_kwargs = {
            'url': {'view_name': 'product-detail', 'lookup_field': 'slug'}
        }

8. Serializer Inheritance

Create base serializers and extend them:

python
class BaseProductSerializer(serializers.ModelSerializer):
    """Base serializer with common fields"""

    class Meta:
        model = Product
        fields = ['id', 'name', 'slug', 'price']
        read_only_fields = ['id', 'slug']


class ProductPublicSerializer(BaseProductSerializer):
    """Public API serializer"""
    category_name = serializers.CharField(source='category.name')

    class Meta(BaseProductSerializer.Meta):
        fields = BaseProductSerializer.Meta.fields + [
            'description',
            'category_name',
        ]


class ProductAdminSerializer(BaseProductSerializer):
    """Admin API serializer with extra fields"""
    category = CategorySerializer(read_only=True)

    class Meta(BaseProductSerializer.Meta):
        fields = BaseProductSerializer.Meta.fields + [
            'description',
            'stock',
            'cost',
            'category',
            'active',
            'featured',
            'created_at',
            'updated_at',
        ]
        read_only_fields = BaseProductSerializer.Meta.read_only_fields + [
            'created_at',
            'updated_at',
        ]

9. File Upload Serializers

Handle file uploads properly:

python
class ProductImageSerializer(serializers.ModelSerializer):
    image = serializers.ImageField(required=True)
    image_url = serializers.SerializerMethodField()

    class Meta:
        model = ProductImage
        fields = ['id', 'image', 'image_url', 'alt_text', 'order']

    def get_image_url(self, obj):
        """Get absolute URL for image"""
        request = self.context.get('request')
        if obj.image and request:
            return request.build_absolute_uri(obj.image.url)
        return None

    def validate_image(self, value):
        """Validate image file"""
        # Check file size (5MB max)
        if value.size > 5 * 1024 * 1024:
            raise serializers.ValidationError("Image size cannot exceed 5MB")

        # Check file type
        allowed_types = ['image/jpeg', 'image/png', 'image/webp']
        if value.content_type not in allowed_types:
            raise serializers.ValidationError(
                f"Unsupported file type. Allowed: {', '.join(allowed_types)}"
            )

        return value

10. Bulk Operations

Handle bulk create/update:

python
class ProductBulkSerializer(serializers.ListSerializer):
    """Handle bulk product operations"""

    def create(self, validated_data):
        """Bulk create products"""
        products = [Product(**item) for item in validated_data]
        return Product.objects.bulk_create(products)

    def update(self, instance, validated_data):
        """Bulk update products"""
        product_mapping = {product.id: product for product in instance}

        data_mapping = {item['id']: item for item in validated_data}

        # Update existing products
        products_to_update = []
        for product_id, data in data_mapping.items():
            product = product_mapping.get(product_id)
            if product:
                for attr, value in data.items():
                    setattr(product, attr, value)
                products_to_update.append(product)

        Product.objects.bulk_update(
            products_to_update,
            ['name', 'price', 'stock']  # Fields to update
        )

        return products_to_update


class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'stock']
        list_serializer_class = ProductBulkSerializer

Best Practices

  1. Use appropriate read-only fields: Set read_only=True for computed or auto-generated fields
  2. Separate serializers by use case: List, detail, create, update
  3. Validate thoroughly: Add field-level and object-level validation
  4. Optimize nested serializers: Use select_related()/prefetch_related() in views
  5. Document serializers: Add docstrings explaining purpose and usage
  6. Handle errors gracefully: Provide clear validation error messages
  7. Use SerializerMethodField sparingly: Can impact performance
  8. Test serializers: Unit test validation logic and data transformation

Common Patterns Checklist

When creating a serializer:

  • Define appropriate fields (include/exclude)
  • Set read_only_fields for auto-generated fields
  • Add validation for business rules
  • Handle nested relationships appropriately
  • Consider different serializers for different operations
  • Add computed fields if needed (SerializerMethodField)
  • Document expected input/output format
  • Test validation and data transformation

This skill helps you create robust, well-validated DRF serializers efficiently.

Didn't find tool you were looking for?

Be as detailed as possible for better results