Django REST Framework Serializers em Profundidade: Validação, Aninhamento e N+1

Domine os serializers DRF com técnicas avançadas de validação, padrões de serializers aninhados e estratégias de otimização de consultas N+1. Exemplos de código prontos para produção incluídos.

Django REST Framework serializers architecture diagram

Os serializers do Django REST Framework lidam com a tarefa complexa de converter querysets e instâncias de modelos em respostas JSON—e validar dados de entrada antes que cheguem ao banco de dados. Embora o uso básico de serializers pareça simples, aplicações em produção exigem domínio do pipeline de validação, relacionamentos aninhados e otimização de consultas.

Regra de Performance dos Serializers DRF

Cada SerializerMethodField ou serializer aninhado acessando objetos relacionados sem select_related/prefetch_related dispara consultas adicionais ao banco de dados. Uma lista de 100 objetos com 3 relacionamentos significa 301 consultas em vez de 4.

Entendendo o Pipeline de Validação dos Serializers DRF

Os serializers DRF executam validação em uma ordem específica: deserialização no nível do campo, validadores no nível do campo, e então validação no nível do objeto via validate(). Este pipeline determina quando e como interceptar transformações de dados.

A sequência de validação começa com to_internal_value(), que deserializa tipos de dados primitivos e executa validadores de campos. Somente após todos os campos passarem pela validação individual é que validate() executa para verificações entre campos.

python
# serializers.py
from rest_framework import serializers
from django.utils import timezone
from .models import Event

class EventSerializer(serializers.ModelSerializer):
    start_date = serializers.DateTimeField()
    end_date = serializers.DateTimeField()
    
    class Meta:
        model = Event
        fields = ['id', 'title', 'start_date', 'end_date', 'location']
    
    def validate_start_date(self, value):
        # Validação no nível do campo executa primeiro
        if value < timezone.now():
            raise serializers.ValidationError("Start date cannot be in the past.")
        return value
    
    def validate(self, attrs):
        # Validação no nível do objeto executa após todos os campos
        start = attrs.get('start_date')
        end = attrs.get('end_date')
        
        if start and end and end <= start:
            raise serializers.ValidationError({
                'end_date': "End date must be after start date."
            })
        return attrs

Essa separação permite controle granular: capturar valores de campo obviamente inválidos cedo, e então validar regras de negócio que abrangem múltiplos campos.

Validadores Personalizados e Lógica de Validação Reutilizável

DRF suporta três padrões de validadores: métodos no nível do campo, classes de validadores independentes, e o argumento validators. Validadores independentes promovem reutilização entre serializers e mantêm responsabilidade única.

python
# validators.py
from rest_framework import serializers
import re

class SlugFormatValidator:
    """Valida formato de slug compatível com URL."""
    
    def __init__(self, allow_unicode=False):
        self.allow_unicode = allow_unicode
        self.pattern = r'^[\w-]+$' if allow_unicode else r'^[a-z0-9-]+$'
    
    def __call__(self, value):
        if not re.match(self.pattern, value):
            raise serializers.ValidationError(
                "Slug must contain only lowercase letters, numbers, and hyphens."
            )

class UniqueForUserValidator:
    """Valida unicidade limitada ao usuário atual."""
    requires_context = True
    
    def __init__(self, queryset, field):
        self.queryset = queryset
        self.field = field
    
    def __call__(self, value, serializer_field):
        request = serializer_field.context.get('request')
        if not request or not request.user.is_authenticated:
            return
        
        queryset = self.queryset.filter(
            user=request.user,
            **{self.field: value}
        )
        
        # Excluir instância atual durante atualizações
        instance = serializer_field.parent.instance
        if instance:
            queryset = queryset.exclude(pk=instance.pk)
        
        if queryset.exists():
            raise serializers.ValidationError(
                f"You already have an item with this {self.field}."
            )

Aplicar validadores de forma declarativa mantém as classes de serializers focadas na estrutura em vez da implementação de validação.

python
# serializers.py
from .validators import SlugFormatValidator, UniqueForUserValidator
from .models import Project

class ProjectSerializer(serializers.ModelSerializer):
    slug = serializers.CharField(
        max_length=100,
        validators=[
            SlugFormatValidator(),
            UniqueForUserValidator(Project.objects.all(), 'slug')
        ]
    )
    
    class Meta:
        model = Project
        fields = ['id', 'name', 'slug', 'description']

Serializers Aninhados: Relacionamentos Graváveis Implementados Corretamente

Serializers aninhados permitem ler e escrever objetos relacionados em uma única requisição. O desafio está em lidar com criação, atualizações e manter integridade referencial através dos relacionamentos.

Para operações de leitura, serializers aninhados funcionam automaticamente. Operações de escrita requerem sobrescritas explícitas dos métodos create() e update() já que DRF não consegue inferir como lidar com dados aninhados.

python
# models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField(unique=True)

class Book(models.Model):
    title = models.CharField(max_length=300)
    isbn = models.CharField(max_length=13, unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    
class Chapter(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='chapters')
    number = models.PositiveIntegerField()
    title = models.CharField(max_length=200)

O serializer lida com criação e atualização de capítulos aninhados dentro de um livro:

python
# serializers.py
from rest_framework import serializers
from django.db import transaction
from .models import Author, Book, Chapter

class ChapterSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField(required=False)  # Permitir ID para atualizações
    
    class Meta:
        model = Chapter
        fields = ['id', 'number', 'title']

class BookSerializer(serializers.ModelSerializer):
    chapters = ChapterSerializer(many=True)
    author_name = serializers.CharField(source='author.name', read_only=True)
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'isbn', 'author', 'author_name', 'chapters']
    
    @transaction.atomic
    def create(self, validated_data):
        chapters_data = validated_data.pop('chapters', [])
        book = Book.objects.create(**validated_data)
        
        Chapter.objects.bulk_create([
            Chapter(book=book, **chapter_data)
            for chapter_data in chapters_data
        ])
        return book
    
    @transaction.atomic
    def update(self, instance, validated_data):
        chapters_data = validated_data.pop('chapters', [])
        
        # Atualizar campos do livro
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        
        # Rastrear capítulos existentes para detecção de exclusão
        existing_ids = set(instance.chapters.values_list('id', flat=True))
        updated_ids = set()
        
        for chapter_data in chapters_data:
            chapter_id = chapter_data.pop('id', None)
            
            if chapter_id and chapter_id in existing_ids:
                # Atualizar capítulo existente
                Chapter.objects.filter(id=chapter_id).update(**chapter_data)
                updated_ids.add(chapter_id)
            else:
                # Criar novo capítulo
                Chapter.objects.create(book=instance, **chapter_data)
        
        # Excluir capítulos não incluídos na requisição
        instance.chapters.filter(id__in=existing_ids - updated_ids).delete()
        
        return instance

O decorator @transaction.atomic garante que todas as operações aninhadas tenham sucesso ou falhem juntas—crítico para consistência de dados em APIs de produção.

Armadilha de Atualização em Serializers Aninhados

Sem tratamento explícito de ID em serializers aninhados, cada requisição de atualização cria novos objetos relacionados em vez de modificar os existentes. Sempre incluir id = serializers.IntegerField(required=False) para objetos aninhados atualizáveis.

Pronto para mandar bem nas entrevistas de Django?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Resolvendo o Problema de Consultas N+1 em Serializers DRF

Consultas N+1 ocorrem ao serializar listas com objetos relacionados. Cada item na lista dispara consultas separadas para seus relacionamentos, devastando os tempos de resposta da API. Os padrões de otimização de consultas Django ORM se aplicam diretamente ao DRF.

Considere uma view retornando 50 livros com seus autores e capítulos:

python
# views.py - PROBLEMÁTICO: consultas N+1
from rest_framework import generics
from .models import Book
from .serializers import BookSerializer

class BookListView(generics.ListAPIView):
    queryset = Book.objects.all()  # 1 consulta para livros
    serializer_class = BookSerializer  # +50 consultas para autores, +50 para capítulos = 101 total

A solução requer select_related para chaves estrangeiras e prefetch_related para relações inversas:

python
# views.py - OTIMIZADO: 3 consultas no total
class BookListView(generics.ListAPIView):
    queryset = Book.objects.select_related('author').prefetch_related('chapters')
    serializer_class = BookSerializer

Para serializers complexos com lógica condicional, sobrescrever get_queryset() para corresponder aos requisitos do serializer:

python
# views.py
from rest_framework import viewsets
from django.db.models import Prefetch, Count
from .models import Author
from .serializers import AuthorDetailSerializer

class AuthorViewSet(viewsets.ModelViewSet):
    serializer_class = AuthorDetailSerializer
    
    def get_queryset(self):
        return Author.objects.prefetch_related(
            Prefetch(
                'books',
                queryset=Book.objects.select_related('publisher').annotate(
                    chapter_count=Count('chapters')
                ).order_by('-publication_date')
            )
        )

Otimização de Performance do SerializerMethodField

SerializerMethodField executa código Python para cada instância serializada. Realizar consultas ao banco de dados dentro desses métodos cria problemas N+1 ocultos que não aparecem nos logs de consultas padrão.

python
# serializers.py - PROBLEMÁTICO
class AuthorSerializer(serializers.ModelSerializer):
    total_sales = serializers.SerializerMethodField()
    
    class Meta:
        model = Author
        fields = ['id', 'name', 'total_sales']
    
    def get_total_sales(self, obj):
        # Consulta executada para CADA autor na lista
        return obj.books.aggregate(total=Sum('sales'))['total'] or 0

A solução move a agregação para o nível do queryset:

python
# views.py
from django.db.models import Sum

class AuthorListView(generics.ListAPIView):
    queryset = Author.objects.annotate(
        total_sales=Sum('books__sales')
    )
    serializer_class = AuthorSerializer
python
# serializers.py - OTIMIZADO
class AuthorSerializer(serializers.ModelSerializer):
    total_sales = serializers.IntegerField(read_only=True)  # Da anotação
    
    class Meta:
        model = Author
        fields = ['id', 'name', 'total_sales']

O campo anotado se torna um campo de serializer regular, eliminando consultas por instância completamente.

Seleção Dinâmica de Campos com Contexto do Serializer

APIs de produção frequentemente precisam de flexibilidade de campos—clientes mobile querem payloads mínimos enquanto dashboards de administração requerem todos os dados. Serializers dinâmicos adaptam a saída baseada no contexto da requisição.

python
# serializers.py
class DynamicFieldsMixin:
    """Permite seleção de campos via parâmetro de consulta ?fields=id,name,email."""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        request = self.context.get('request')
        if not request:
            return
        
        fields_param = request.query_params.get('fields')
        if fields_param:
            requested = set(fields_param.split(','))
            existing = set(self.fields.keys())
            
            # Remover campos não solicitados
            for field_name in existing - requested:
                self.fields.pop(field_name)

class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined']

Requisições para /api/users/?fields=id,username retornam apenas esses campos, reduzindo o tamanho do payload e potencialmente permitindo otimização adicional de consultas baseada nos campos selecionados.

Disponibilidade do Contexto do Serializer

O contexto só é preenchido quando o serializer é instanciado com uma requisição. Instanciação direta como UserSerializer(data=payload) tem contexto vazio—sempre passar context={'request': request} em ViewSets ou Views.

Monitoramento de Performance e Análise de Consultas

Identificar problemas de consultas induzidos por serializers requer visibilidade nas operações de banco de dados. Django Debug Toolbar e django-silk fornecem análise de consultas por requisição durante o desenvolvimento.

Para monitoramento em produção, registrar consultas lentas e rastrear tempo de serialização:

python
# middleware.py
import time
import logging
from django.db import connection, reset_queries
from django.conf import settings

logger = logging.getLogger('api.performance')

class QueryCountMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        reset_queries()
        start = time.perf_counter()
        
        response = self.get_response(request)
        
        duration = time.perf_counter() - start
        query_count = len(connection.queries)
        
        if query_count > settings.QUERY_COUNT_WARNING_THRESHOLD:
            logger.warning(
                'High query count: %d queries in %.2fs for %s %s',
                query_count, duration, request.method, request.path
            )
        
        return response

Definir QUERY_COUNT_WARNING_THRESHOLD baseado na complexidade da API—endpoints que retornam listas tipicamente precisam de 3-5 consultas independente do tamanho da página.

Para uma preparação completa para entrevistas Django REST Framework, entender esses padrões de serializers distingue engenheiros seniores daqueles que conhecem apenas o uso básico.

Conclusão

  • A ordem de validação importa: validadores no nível do campo executam antes de validate(), permitindo falha antecipada para dados obviamente inválidos
  • Extrair validadores reutilizáveis: classes de validadores independentes com requires_context = True acessam dados da requisição enquanto permanecem testáveis
  • Escritas aninhadas precisam de tratamento explícito: sobrescrever create() e update() com @transaction.atomic para integridade de dados
  • Corresponder queryset ao serializer: cada serializer aninhado e SerializerMethodField acessando relacionamentos requer select_related/prefetch_related correspondente
  • Anotar em vez de calcular: mover cálculos de SerializerMethodField para anotações de queryset para eliminar consultas por instância
  • Monitorar contagem de consultas: APIs de produção devem rastrear consultas ao banco de dados por requisição para detectar regressões N+1

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Compartilhar

Artigos relacionados