# Mendalami Serializer Django REST Framework: Validasi, Nested, dan Optimasi N+1 > Panduan lengkap menguasai DRF serializers: pipeline validasi, nested serializers, penanganan relasi kompleks, dan teknik optimasi untuk menghindari N+1 queries. - Published: 2026-07-12 - Updated: 2026-07-12 - Author: SharpSkill - Tags: django, drf, serializers, api, python - Reading time: 5 min --- Serializer pada Django REST Framework mengemban tugas penting dalam mengkonversi queryset dan instance model menjadi respons JSON—serta memvalidasi data yang masuk sebelum tersimpan di database. Meskipun penggunaan dasar serializer tampak sederhana, aplikasi produksi menuntut penguasaan pipeline validasi, penanganan relasi nested, dan optimasi query yang tepat. > **Aturan Performa DRF Serializer** > > Setiap `SerializerMethodField` atau nested serializer yang mengakses objek relasi tanpa `select_related`/`prefetch_related` akan memicu query database tambahan. Daftar 100 objek dengan 3 relasi berarti 301 queries, bukan 4. ## Memahami Pipeline Validasi Serializer DRF Serializer DRF mengeksekusi validasi dalam urutan spesifik: deserialisasi tingkat field, validator tingkat field, kemudian validasi tingkat objek melalui `validate()`. Pipeline ini menentukan kapan dan bagaimana mencegat transformasi data. Urutan validasi dimulai dengan `to_internal_value()`, yang melakukan deserialisasi tipe data primitif dan menjalankan validator field. Hanya setelah semua field lolos validasi individual, barulah `validate()` dieksekusi untuk pemeriksaan lintas field. ```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): # Validasi tingkat field berjalan pertama if value < timezone.now(): raise serializers.ValidationError("Tanggal mulai tidak boleh di masa lampau.") return value def validate(self, attrs): # Validasi tingkat objek berjalan setelah semua field tervalidasi start = attrs.get('start_date') end = attrs.get('end_date') if start and end and end <= start: raise serializers.ValidationError({ 'end_date': "Tanggal akhir harus setelah tanggal mulai." }) return attrs ``` Pemisahan ini memungkinkan kontrol granular: menangkap nilai field yang jelas invalid lebih awal, kemudian memvalidasi aturan bisnis yang mencakup beberapa field. ## Validator Kustom dan Logika Validasi yang Dapat Digunakan Ulang DRF mendukung tiga pola validator: metode tingkat field, kelas validator mandiri, dan argumen `validators`. Validator mandiri mendorong penggunaan ulang di berbagai serializer dan mempertahankan prinsip single responsibility. ```python # validators.py from rest_framework import serializers import re class SlugFormatValidator: """Memvalidasi format slug yang aman untuk 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 hanya boleh berisi huruf kecil, angka, dan tanda hubung." ) class UniqueForUserValidator: """Memvalidasi bahwa nilai unik untuk user saat ini.""" requires_context = True def __init__(self, queryset, field_name): self.queryset = queryset self.field_name = field_name def __call__(self, value, serializer_field): request = serializer_field.context.get('request') if not request or not request.user.is_authenticated: return filter_kwargs = { self.field_name: value, 'user': request.user } # Kecualikan instance saat ini saat update instance = serializer_field.parent.instance exists_query = self.queryset.filter(**filter_kwargs) if instance: exists_query = exists_query.exclude(pk=instance.pk) if exists_query.exists(): raise serializers.ValidationError( f"Anda sudah memiliki item dengan {self.field_name} ini." ) ``` Validator yang memerlukan context menggunakan flag `requires_context = True` dan menerima `serializer_field` sebagai argumen kedua. Ini memberikan akses ke request, instance, dan seluruh serializer parent. ```python # serializers.py from .validators import SlugFormatValidator, UniqueForUserValidator from .models import Article class ArticleSerializer(serializers.ModelSerializer): slug = serializers.CharField( max_length=100, validators=[ SlugFormatValidator(allow_unicode=False), UniqueForUserValidator( queryset=Article.objects.all(), field_name='slug' ) ] ) class Meta: model = Article fields = ['id', 'title', 'slug', 'content', 'published'] ``` ## Menangani Nested Serializer untuk Relasi Kompleks Nested serializer memodelkan relasi database dalam respons API. Tantangan sebenarnya terletak pada penanganan operasi create dan update—terutama ketika berhubungan dengan relasi many-to-many atau reverse foreign key. ```python # models.py from django.db import models class Author(models.Model): name = models.CharField(max_length=200) email = models.EmailField(unique=True) class Tag(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(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') tags = models.ManyToManyField(Tag, related_name='books') published_date = models.DateField() class Chapter(models.Model): book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='chapters') title = models.CharField(max_length=200) order = models.PositiveIntegerField() content = models.TextField() ``` Nested serializer read-only dapat langsung disertakan, tetapi nested yang dapat ditulis memerlukan override `create()` dan `update()` eksplisit. ```python # serializers.py from rest_framework import serializers from django.db import transaction from .models import Author, Tag, Book, Chapter class ChapterSerializer(serializers.ModelSerializer): class Meta: model = Chapter fields = ['id', 'title', 'order', 'content'] class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag fields = ['id', 'name', 'slug'] class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author fields = ['id', 'name', 'email'] class BookSerializer(serializers.ModelSerializer): author = AuthorSerializer(read_only=True) author_id = serializers.PrimaryKeyRelatedField( queryset=Author.objects.all(), source='author', write_only=True ) tags = TagSerializer(many=True, read_only=True) tag_ids = serializers.PrimaryKeyRelatedField( queryset=Tag.objects.all(), source='tags', many=True, write_only=True ) chapters = ChapterSerializer(many=True) class Meta: model = Book fields = [ 'id', 'title', 'isbn', 'author', 'author_id', 'tags', 'tag_ids', 'chapters', 'published_date' ] @transaction.atomic def create(self, validated_data): chapters_data = validated_data.pop('chapters', []) tags = validated_data.pop('tags', []) book = Book.objects.create(**validated_data) book.tags.set(tags) for chapter_data in chapters_data: Chapter.objects.create(book=book, **chapter_data) return book @transaction.atomic def update(self, instance, validated_data): chapters_data = validated_data.pop('chapters', None) tags = validated_data.pop('tags', None) # Update field sederhana for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() # Update relasi many-to-many jika disediakan if tags is not None: instance.tags.set(tags) # Tangani update nested chapters if chapters_data is not None: # Strategi: hapus yang ada dan buat ulang instance.chapters.all().delete() for chapter_data in chapters_data: Chapter.objects.create(book=instance, **chapter_data) return instance ``` Pola ini memisahkan operasi read dan write: field nested menyediakan output terformat dengan baik, sementara field `*_id` menerima primary key untuk input. Wrapper `@transaction.atomic` memastikan konsistensi data ketika operasi multiple gagal sebagian. ## Teknik Optimasi untuk Menghindari Masalah N+1 Query Masalah performa paling umum pada DRF berasal dari query N+1 yang dihasilkan oleh nested serializer dan method field. Sebuah endpoint yang mengembalikan 50 buku dengan author, tag, dan chapter akan mengeksekusi ratusan query tanpa optimasi yang tepat. ```python # views.py from rest_framework import viewsets from django.db.models import Prefetch from .models import Book, Chapter from .serializers import BookSerializer class BookViewSet(viewsets.ModelViewSet): serializer_class = BookSerializer def get_queryset(self): return Book.objects.select_related( 'author' ).prefetch_related( 'tags', Prefetch( 'chapters', queryset=Chapter.objects.order_by('order') ) ) ``` Pilihan antara `select_related` dan `prefetch_related` mengikuti aturan sederhana: `select_related` untuk relasi single-object (ForeignKey, OneToOne) menggunakan SQL JOIN, sedangkan `prefetch_related` untuk relasi multi-object (ManyToMany, reverse ForeignKey) menggunakan query terpisah. ### Menangani SerializerMethodField dengan Efisien `SerializerMethodField` sering menyebabkan masalah N+1 karena query dieksekusi per instance. Solusinya melibatkan anotasi queryset atau penggunaan prefetch yang cermat. ```python # serializers.py class BookListSerializer(serializers.ModelSerializer): author_name = serializers.CharField(source='author.name', read_only=True) chapter_count = serializers.IntegerField(read_only=True) tag_names = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author_name', 'chapter_count', 'tag_names'] def get_tag_names(self, obj): # Aman jika tags sudah di-prefetch return [tag.name for tag in obj.tags.all()] # views.py from django.db.models import Count class BookListViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = BookListSerializer def get_queryset(self): return Book.objects.select_related( 'author' ).prefetch_related( 'tags' ).annotate( chapter_count=Count('chapters') ) ``` Anotasi `Count` menghitung chapter dalam query tunggal alih-alih mengambil relasi dan menghitung di Python. Akses `author.name` melalui source berfungsi karena `select_related('author')` sudah memuat data author. ### Prefetch Kustom untuk Relasi Tersaring Objek `Prefetch` memungkinkan query kustom untuk relasi yang sudah di-prefetch, memungkinkan filtering dan ordering tanpa query tambahan. ```python # views.py from django.db.models import Prefetch from .models import Book, Chapter, Review class BookDetailViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = BookDetailSerializer def get_queryset(self): return Book.objects.select_related( 'author' ).prefetch_related( 'tags', Prefetch( 'chapters', queryset=Chapter.objects.order_by('order').only( 'id', 'title', 'order', 'book_id' ) ), Prefetch( 'reviews', queryset=Review.objects.filter( is_approved=True ).select_related('user').order_by('-created_at')[:5], to_attr='recent_reviews' ) ) ``` Atribut `to_attr` menyimpan hasil prefetch dalam atribut kustom (`recent_reviews`) alih-alih mengganti manager relasi default. Ini berguna saat diperlukan akses ke subset tersaring dan relasi penuh. ## Kontrol Serialisasi Lanjutan dengan to_representation Method `to_representation()` memberikan kontrol penuh atas output yang diserialisasi. Ini mengeksekusi setelah semua serialisasi field, memungkinkan transformasi berdasarkan context atau data instance. ```python # serializers.py class BookSerializer(serializers.ModelSerializer): chapters = ChapterSerializer(many=True, read_only=True) class Meta: model = Book fields = ['id', 'title', 'isbn', 'author', 'chapters', 'published_date'] def to_representation(self, instance): data = super().to_representation(instance) request = self.context.get('request') # Hilangkan field sensitif untuk user non-staff if request and not request.user.is_staff: data.pop('isbn', None) # Tambahkan field terkondisi if instance.published_date: from django.utils import timezone data['is_new_release'] = ( timezone.now().date() - instance.published_date ).days < 30 # Restrukturisasi output bersarang if 'chapters' in data: data['chapter_count'] = len(data['chapters']) data['chapters'] = data['chapters'][:3] # Hanya 3 pertama di respons return data ``` ## Mendebug Query Serializer Paket `django-debug-toolbar` dan `django-silk` sangat berharga untuk mengidentifikasi masalah query. Untuk logging programatis, wrapper queryset sederhana sudah cukup. ```python # utils.py import logging from django.db import connection, reset_queries from django.conf import settings from functools import wraps logger = logging.getLogger(__name__) def log_queries(func): @wraps(func) def wrapper(*args, **kwargs): if not settings.DEBUG: return func(*args, **kwargs) reset_queries() result = func(*args, **kwargs) queries = connection.queries logger.debug( f"{func.__name__} executed {len(queries)} queries" ) for query in queries: logger.debug(f" [{query['time']}s] {query['sql'][:100]}") return result return wrapper # views.py class BookViewSet(viewsets.ModelViewSet): @log_queries def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) ``` ## Kesimpulan Menguasai serializer DRF memerlukan pemahaman pipeline validasi, pola nested relationship, dan strategi optimasi query. Komponen-komponen kunci yang perlu diingat meliputi: menggunakan validasi tingkat field untuk pemeriksaan individual dan `validate()` untuk aturan lintas field, memisahkan field read-only dan write-only untuk nested relationship yang bersih, selalu menerapkan `select_related` dan `prefetch_related` dalam queryset view, dan memanfaatkan `to_representation()` untuk output bersyarat. Penerapan pola-pola ini memastikan API tidak hanya berfungsi dengan benar tetapi juga berkinerja baik di bawah beban produksi yang nyata. Serializer yang efisien mengurangi waktu respons dan beban server secara signifikan. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/django/django-rest-framework-serializers-deep-dive