Featured image of post Kustomisasi Serializer di Django REST Framework

Kustomisasi Serializer di Django REST Framework

Penjelasan mengenai cara customize serializer DRF menggunakan SerializerMethodField, custom __init__, to_representation, dan to_internal_value.

Di materi-materi sebelumnya, kita sudah belajar bagaimana serializer melakukan validasi data masuk (incoming) dan melakukan penyesuaian data yang keluar (outgoing). Tapi terkadang pendekatan dengan melakukan deklarasi field-by-field saja tidak cukup.

API di production sering juga membutuhkan:

  • Value hasil komputasi yang tidak disimpan sebagai kolom di database (contoh: sisa hari menuju deadline, harga yang sudah diformat, atau derived status label*)
  • Field sets yang berubah-ubah tergantung siapa yang memanggil API (akses admin bisa mengakses data secara lengkap, sementara dengan akses public, hanya melihat data yang sudah di-mask)
  • Transformasi output custom sebelum response dikirimkan (merubah nama key, mengubah format dari value, atau menggabungkan beberapa field)
  • Parsing input custom sebelum validasi dilakukan (menghapus noise formatting dari nomor telepon, standarisasi huruf besar/kecil)

*Status label artinya string yang menunjukkan state dari sesuatu, seperti Open, In Progress, Overdue, Closed, dll. Derived status label artinya status label yang dihitung/diturunkan dari data lain, bukan disimpan langsung di database. Sebagai contoh, di database kita menyimpan field deadline, tapi tidak menyimpan state “Overdue”. State “Overdue” itu dihitung saat user query API. Jika deadline lebih kecil dari date.today(), maka output yang ditampilkan adalah “Overdue”.

Keempat kebutuhan ini akan kita bahas di materi kali ini:

ToolMasalah Yang Diselesaikan
SerializerMethodFieldValue hasil komputasi di output yang tidak punya kolom di database
Custom __init__Field yang muncul secara dinamis tergantung context
to_representation()Kontrol penuh atas apa yang keluar dari serializer
to_internal_value()Kontrol penuh atas apa yang masuk ke serializer

SerializerMethodField

SerializerMethodField adalah field read-only yang memanggil method yang kita definisikan sendiri. Method tersebut menghitung value pada saat serialization berjalan.

Naming convention-nya sangat strict: kalau field kita bernama days_remaining, method-nya harus bernama get_days_remaining.

1
2
3
4
5
6
7
8
9
from datetime import date

class CalculateDaySerializer(serializers.ModelSerializer):
    days_remaining = serializers.SerializerMethodField()

    def get_days_remaining(self, obj):
        # obj adalah model instance yang sedang di-serialize
        delta = obj.deadline - date.today()
        return delta.days  # negatif kalau deadline sudah lewat

Parameter obj adalah instance dari model tersebut. Kita punya akses penuh ke semua field dan method-nya.

Apa yang membuat SerializerMethodField berguna

Ini menjaga logic komputasi tetap ada di dalam serializer, lebih dekat dengan tempat di mana output-nya didefinisikan. Alternatifnya, menambahkan @property ke model kita, bisa saja dilakukan, tapi ini akan menggabungkan logic presentasi ke layer model. SerializerMethodField dapat membantu batas layer model dan bagaimana data nantinya dipresentasikan tetap bersih.

Kapan sebaiknya TIDAK digunakan

Kalau value tersebut memang seharusnya berada di database (misalnya, cached count yang sering di-query), model property atau annotation adalah pilihan yang lebih baik. SerializerMethodField cocok untuk komputasi yang ringan dan cepat, yang secara natural diturunkan dari field yang sudah ada.


Custom __init__

Serializer adalah class Python. Sama seperti class python lainnya, kita dapat melakukan override __init__ di serializer untuk memodifikasi field mana saja yang ada pada saat proses serialization atau deserialization berjalan.

Penggunaan custom __init__ yang paling umum adalah context-based field filtering di mana view mengirimkan data melalui context milik serializer, kemudian __init__ menggunakan value tersebut untuk include atau exclude field tertentu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class SomeSerializer(serializers.ModelSerializer):
    class Meta:
        model = SomeModel
        fields = ["id", "name", "secret_field"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # context baru dapat diakses setelah super().__init__() dipanggil
        show_secret = self.context.get("show_secret", False)
        if not show_secret:
            self.fields.pop("secret_field", None)

View kemudian mengirimkan context saat instantiate:

1
serializer = SomeSerializer(instance, context={"show_secret": True, "request": request})

Kenapa pendekatan ini ada

Alternatifnya, jika tidak menggunakan custom __init__, kita dapat membuat dua serializer yang berbeda (satu untuk admin, satu untuk public caller). Cara seperti itu bisa dilakukan, tapi akan mejadi duplikasi kalau kedua serializer tersebut sebagian besar strukturnya sama. Custom __init__ memungkinkan satu serializer melayani kedua kasus tersebut dengan lebih rapi.

Hal penting yang harus diingat

Selalu panggil super().__init__(*args, **kwargs) sebelum mengakses self.fields atau self.context. __init__ milik parent class yang menyiapkan keduanya. Mengakses self.fields dan self.context sebelum memanggil super akan menghasilkan AttributeError.


Override to_representation()

to_representation(instance) adalah method yang dipanggil DRF untuk mengubah instance dari model menjadi dictionary Python yang nantinya menjadi JSON. Secara default, method ini membuat dict dari semua field yang dideklarasikan.

Kita bisa meng-override method ini untuk:

  • Me-rename key di output
  • Mask atau redact value yang sensitif berdasarkan kondisi tertentu
  • Menambahkan value hasil komputasi yang awkward kalau diekspresikan menggunakan SerializerMethodField
  • Mengubah format dari value (misalnya, mengubah integer yang tersimpan menjadi display string)
  • Menghapus key secara kondisional berdasarkan state dari instance

Penjelasan sederhana

Bayangkan database kita menyimpan sebuah project seperti ini:

1
{ "price_cents": 150000, "status": "closed", "client_email": "john@example.com" }

Tapi kita ingin API response-nya seperti ini:

1
{ "price": "$1,500.00", "client_email": "j***@example.com" }

Data di database tidak pernah berubah, kita hanya menampilkannya secara berbeda sebelum dikirim keluar. Hal itulah yang persis dilakukan to_representation().

Pattern

1
2
3
4
5
def to_representation(self, instance):
    data = super().to_representation(instance)  # Langkah 1: ambil dict normalnya
    # Langkah 2: modifikasi dict-nya
    # Langkah 3: return dict-nya
    return data

Langkah 1: Panggil super() terlebih dahulu. Ini menjalankan logic normal DRF dan memberi kita dictionary biasa yang sudah berisi semua field-nya. Kita tidak pernah membuat dict ini dari nol.

Langkah 2: Edit dictionary tersebut sesuai kebutuhan kita.

Langkah 3: Return dictionary yang sudah dimodifikasi. Ini yang akan menjadi JSON response.

Contoh konkret menggunakan analogi di atas

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def to_representation(self, instance):
    data = super().to_representation(instance)

    # Rename "price_cents" jadi "price" dan format sebagai dollar
    price_cents = data.pop("price_cents")        # menghapus "price_cents": 150000
    data["price"] = f"${price_cents / 100:,.2f}" # menambahkan "price": "$1,500.00"

    # Mask email: "john@example.com" → "j***@example.com"
    email = data["client_email"]
    data["client_email"] = email[0] + "***@" + email.split("@")[1]

    # Project sudah closed, jadi hapus price sepenuhnya
    if instance.status == "closed":
        data.pop("price", None)

    return data

Mari kita telusuri langkah demi langkah dengan value yang sebenarnya:

  1. data.pop("price_cents") mengambil 150000, lalu 150000 / 100 = 1500.0, diformat menjadi "$1,500.00"
  2. "john@example.com"[0]"j", di-split dengan "@" menghasilkan "example.com", hasilnya "j***@example.com"
  3. Karena status == "closed", price dihapus dari output final

to_representation vs SerializerMethodField

Keduanya bisa menambahkan value hasil komputasi ke output. Bedanya ada di scope-nya:

  • SerializerMethodField bersifat self-contained: satu method, satu field, mudah dibaca sekilas.
  • to_representation digunakan kalau kita perlu memodifikasi bentuk keseluruhan output: menghapus key, me-rename key, atau membuat keputusan yang melibatkan beberapa field sekaligus.

Kalau kita hanya butuh satu field tambahan hasil komputasi, gunakan SerializerMethodField. Kalau kita perlu membentuk ulang keseluruhan dict output, gunakan to_representation.


Override to_internal_value()

to_internal_value(data) adalah method yang dipanggil DRF pada data yang masuk sebelum validasi di level field berjalan. Method ini mengubah raw input (request body berupa Python dict) menjadi dict yang sudah bersih, yang kemudian diproses oleh field validators.

Kita meng-override method ini untuk:

  • Menghapus noise formatting dari raw input (misalnya, menghapus spasi dan dash dari nomor telepon sebelum validasi)
  • Normalisasi huruf besar/kecil (lowercase email sebelum sampai ke unique validator)
  • Inject value hasil komputasi yang bergantung pada beberapa field yang masuk sekaligus

Pattern-nya:

1
2
3
4
5
6
def to_internal_value(self, data):
    # Mutate atau preprocess 'data' sebelum masuk ke validasi normal
    data = data.copy()  # hindari mutate dict aslinya
    if "some_field" in data:
        data["some_field"] = preprocess(data["some_field"])
    return super().to_internal_value(data)

to_internal_value vs validate_<field>

Keduanya berjalan pada data yang masuk. Bedanya ada di timing-nya: Keduanya ada di posisi jalur masuk data. Perbedaannya ada pada timing:

  • to_internal_value berjalan sebelum type parsing dan validasi di level field. Value-nya masih berupa raw string dari request body.
  • validate_<field> berjalan setelah type parsing. Value-nya sudah diubah ke tipe Python yang benar (misalnya, string "25" sudah menjadi integer 25).

Gunakan to_internal_value untuk preprocessing yang harus terjadi sebelum type conversion, seperti menghapus karakter formatting. Gunakan validate_<field> untuk menerapkan business rule pada value yang type-nya sudah benar.


Latihan

Lab ini TIDAK melanjutkan app dari materi sebelumnya. Jangan gunakan ulang app yang sudah ada.

Kita akan membangun project board freelance yang sederhana. Client mem-posting entry FreelanceProject dengan budget (disimpan dalam bentuk cents supaya terhindar dari masalah floating-point), deadline, dan status. Latihan ini akan mempraktikkan keempat customization tool di atas pada satu model yang sama ini.


Buat app dan daftarkan

1
2
3
cd my-drf-project
source .venv/bin/activate
python manage.py startapp customization_lab

Tambahkan ke INSTALLED_APPS di config/settings.py:

1
2
3
4
INSTALLED_APPS = [
    # ...existing apps...
    "customization_lab",
]

Buat model di customization_lab/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from django.db import models


class FreelanceProject(models.Model):
    class Status(models.TextChoices):
        OPEN = "open", "Open"
        IN_PROGRESS = "in_progress", "In Progress"
        CLOSED = "closed", "Closed"

    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    budget_cents = models.PositiveIntegerField(
        help_text="Budget tersimpan dalam cents. 150000 artinya $1,500.00."
    )
    deadline = models.DateField()
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.OPEN,
    )
    client_email = models.EmailField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["deadline"]

    def __str__(self):
        return self.title

Kenapa budget_cents? Menyimpan uang sebagai integer (cents) menghindari bug floating-point precision. $1,500.00 disimpan sebagai 150000. Serializer nanti yang akan melakukan konversi ke display string yang display-friendly.


Buat dan apply migrations

1
2
python manage.py makemigrations customization_lab
python manage.py migrate

Tambahkan sample data ke database

1
python manage.py shell
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from datetime import date, timedelta
from customization_lab.models import FreelanceProject

FreelanceProject.objects.all().delete()

today = date.today()

FreelanceProject.objects.bulk_create([
    FreelanceProject(
        title="Build REST API for Inventory System",
        description="DRF-based API with full CRUD and auth.",
        budget_cents=250000,
        deadline=today + timedelta(days=14),
        status="open",
        client_email="procurement@beta-corp.io",
    ),
    FreelanceProject(
        title="Django Admin Customization",
        description="Custom list filters, inlines, and export.",
        budget_cents=80000,
        deadline=today + timedelta(days=3),
        status="open",
        client_email="dev@startupx.co",
    ),
    FreelanceProject(
        title="Payment Gateway Integration",
        description="Stripe integration with webhook handling.",
        budget_cents=400000,
        deadline=today - timedelta(days=5),
        status="in_progress",
        client_email="ops@fintech-alpha.com",
    ),
    FreelanceProject(
        title="Legacy Code Refactor",
        description="Migrate spaghetti views to class-based views.",
        budget_cents=120000,
        deadline=today - timedelta(days=30),
        status="closed",
        client_email="cto@oldstack.net",
    ),
])

print(FreelanceProject.objects.count())

Output yang diharapkan: 4


Buat serializer dengan SerializerMethodField

Buat customization_lab/serializers.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from datetime import date

from rest_framework import serializers

from .models import FreelanceProject


# =============================================================================
# BAGIAN A: SerializerMethodField
# =============================================================================

class FreelanceProjectReadSerializer(serializers.ModelSerializer):
    """
    Demo SerializerMethodField.

    Menambahkan tiga field hasil komputasi yang tidak ada kolom di database:
    - budget_display: string budget yang mudah dibaca, diturunkan dari budget_cents
    - days_remaining: jumlah hari (integer) antara hari ini dan deadline
    - is_accepting_proposals: True hanya kalau status OPEN dan deadline belum lewat
    """

    budget_display = serializers.SerializerMethodField()
    days_remaining = serializers.SerializerMethodField()
    is_accepting_proposals = serializers.SerializerMethodField()

    class Meta:
        model = FreelanceProject
        fields = [
            "id",
            "title",
            "description",
            "budget_cents",
            "budget_display",
            "deadline",
            "days_remaining",
            "status",
            "is_accepting_proposals",
            "client_email",
            "created_at",
        ]
        read_only_fields = ["id", "created_at"]

    def get_budget_display(self, obj):
        # Konversi cents ke dollar dan format sebagai string.
        # 250000 cents -> "$2,500.00"
        dollars = obj.budget_cents / 100
        return f"${dollars:,.2f}"

    def get_days_remaining(self, obj):
        delta = obj.deadline - date.today()
        return delta.days  # negatif kalau deadline sudah lewat

    def get_is_accepting_proposals(self, obj):
        return obj.status == FreelanceProject.Status.OPEN and obj.deadline >= date.today()

Kita akan melakukan test di bagian berikutnya.


Test SerializerMethodField

1
python manage.py shell
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from customization_lab.models import FreelanceProject
from customization_lab.serializers import FreelanceProjectReadSerializer

# Serialize satu project yang berstatus open
project = FreelanceProject.objects.filter(status="open").first()
ser = FreelanceProjectReadSerializer(project)

print(ser.data["title"])
print(ser.data["budget_display"])          # "$2,500.00" atau semacamnya
print(ser.data["days_remaining"])          # integer positif
print(ser.data["is_accepting_proposals"])  # True

# Serialize project yang berstatus in_progress (deadline sudah lewat)
in_progress = FreelanceProject.objects.get(status="in_progress")
ser2 = FreelanceProjectReadSerializer(in_progress)

print(ser2.data["days_remaining"])          # negatif (deadline 5 hari lalu)
print(ser2.data["is_accepting_proposals"])  # False (status bukan OPEN)

# Serialize semua project
all_projects = FreelanceProject.objects.all()
ser_many = FreelanceProjectReadSerializer(all_projects, many=True)

for item in ser_many.data:
    print(f"{item['title'][:30]:<30} | budget: {item['budget_display']} | days: {item['days_remaining']}")

Yang perlu diperhatikan:

  • budget_display dihitung pada saat dipanggil, tidak tersimpan di kolom manapun.
  • days_remaining bernilai negatif untuk deadline yang sudah lewat.
  • is_accepting_proposals menggabungkan dua field menjadi satu boolean.

Tambahkan custom __init__

Tambahkan kode berikut ke customization_lab/serializers.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# =============================================================================
# BAGIAN B: Custom __init__
# =============================================================================

class FreelanceProjectContextSerializer(serializers.ModelSerializer):
    """
    Demo custom __init__ untuk context-based field control.

    Kalau context serializer berisi {"show_client_email": True},
    field client_email akan dimasukkan ke output.

    Kalau show_client_email bernilai False atau tidak ada,
    field tersebut dihapus sebelum serialization berjalan -- caller tidak akan pernah melihatnya.

    Penggunaan dari view:
        FreelanceProjectContextSerializer(
            instance,
            context={"show_client_email": True, "request": request}
        )
    """

    class Meta:                                                                                                                                                                                                                               
        model = FreelanceProject
        fields = ["id", "title", "status", "deadline", "budget_cents", "client_email"]
        read_only_fields = ["id"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # self.context baru bisa diakses setelah super().__init__() berjalan.
        show_email = self.context.get("show_client_email", False)

        if not show_email:
            self.fields.pop("client_email", None)

Kita akan melakukan test di bagian berikutnya.


Test custom __init__

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from customization_lab.models import FreelanceProject
from customization_lab.serializers import FreelanceProjectContextSerializer

project = FreelanceProject.objects.first()

# Tanpa context flag -- email seharusnya tidak muncul
ser_public = FreelanceProjectContextSerializer(project)
print("client_email" in ser_public.data)  # False

# Dengan context flag -- email seharusnya muncul
ser_admin = FreelanceProjectContextSerializer(
    project,
    context={"show_client_email": True}
)
print("client_email" in ser_admin.data)   # True
print(ser_admin.data["client_email"])      # email aslinya

Yang perlu diperhatikan:

  • Serializer class yang sama, model instance yang sama, tapi output-nya berbeda tergantung context.
  • Penghapusan field terjadi di __init__, sebelum data apapun disentuh.

Tambahkan to_representation

Tambahkan kode berikut ke customization_lab/serializers.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# =============================================================================
# BAGIAN C: to_representation()
# =============================================================================

class FreelanceProjectMaskedSerializer(serializers.ModelSerializer):
    """
    Demo to_representation() untuk kontrol output.

    Memodifikasi dict output setelah serialization normal menghasilkannya:
    - Rename "budget_cents" menjadi "budget" di output key
    - Mengubah integer budget menjadi formatted dollar string
    - Mask client email: "ops@fintech-alpha.com" -> "o***@fintech-alpha.com"
    - Menghapus key sepenuhnya kalau status "closed"
    """

    class Meta:
        model = FreelanceProject
        fields = ["id", "title", "status", "budget_cents", "deadline", "client_email"]
        read_only_fields = ["id"]

    def to_representation(self, instance):
        data = super().to_representation(instance)

        # 1) Rename dan reformat budget
        raw_cents = data.pop("budget_cents")
        data["budget"] = f"${raw_cents / 100:,.2f}"

        # 2) Mask client email
        email = data.get("client_email", "")
        if email and "@" in email:
            local, domain = email.split("@", 1)
            masked_local = local[0] + "***"
            data["client_email"] = f"{masked_local}@{domain}"

        # 3) Sembunyikan budget untuk project yang sudah closed
        if data.get("status") == "closed":
            data.pop("budget", None)

        return data

Kita akan melakukan test di bagian berikutnya.


Test to_representation()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from customization_lab.models import FreelanceProject
from customization_lab.serializers import FreelanceProjectMaskedSerializer

# Project yang open
open_project = FreelanceProject.objects.filter(status="open").first()
ser = FreelanceProjectMaskedSerializer(open_project)
print(ser.data)

# Hal-hal penting untuk diverifikasi:
print("budget_cents" in ser.data)   # False -- key sudah di-rename
print("budget" in ser.data)         # True  -- key baru sudah ada
print(ser.data["budget"])           # "$2,500.00" atau semacamnya

# Email harusnya sudah di-mask
print(ser.data["client_email"])     # "p***@beta-corp.io" atau semacamnya

# Project yang closed
closed_project = FreelanceProject.objects.get(status="closed")
ser_closed = FreelanceProjectMaskedSerializer(closed_project)
print("budget" in ser_closed.data)  # False -- dihapus untuk project yang closed

Yang perlu diperhatikan:

  • to_representation memodifikasi dict yang dihasilkan oleh super().
  • Rename, reformat, dan penghapusan kondisional semuanya terjadi di satu tempat.
  • Model instance-nya sendiri tidak pernah berubah.

Tambahkan to_internal_value

Tambahkan kode berikut ke customization_lab/serializers.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# =============================================================================
# BAGIAN D: to_internal_value()
# =============================================================================

class FreelanceProjectWriteSerializer(serializers.ModelSerializer):
    """
    Demo to_internal_value() untuk mengontrol input.

    Preprocess data yang masuk sebelum validasi di level field berjalan:
    - Menghapus whitespace di awal/akhir title
    - Lowercase client_email (supaya "Ops@FinTech.Com" dan "ops@fintech.com" dianggap sama)
    - Mengubah input budget berupa dollar menjadi cents kalau payload mengirim
      budget sebagai angka biasa (client mengirim 1500, kita simpan 150000)

    Catatan: to_internal_value berjalan sebelum type parsing dan field validators,
    jadi value di sini masih berupa raw string dari request body.
    """

    class Meta:
        model = FreelanceProject
        fields = ["id", "title", "description", "budget_cents", "deadline", "status", "client_email"]
        read_only_fields = ["id"]

    def to_internal_value(self, data):
        # Selalu copy -- jangan pernah mutate dict data request aslinya.
        data = data.copy()

        # Normalisasi whitespace title.
        if "title" in data and isinstance(data["title"], str):
            data["title"] = data["title"].strip()

        # Normalisasi huruf besar/kecil email.
        if "client_email" in data and isinstance(data["client_email"], str):
            data["client_email"] = data["client_email"].lower().strip()

        # Konversi dollar ke cents.
        # Kalau client mengirim "budget_cents": 1500 (maksudnya $1.500),
        # kita deteksi bahwa value-nya di bawah threshold kita lalu konversi.
        # Project production sebaiknya mendokumentasikan contract ini dengan jelas di dokumentasi API.
        if "budget_cents" in data:
            try:
                raw = int(data["budget_cents"])
                # Heuristic: kalau value-nya kurang dari 100,
                # kemungkinan besar client mengirim dollar, bukan cents.
                # Kalikan dengan 100 untuk disimpan sebagai cents.
                if raw < 100:
                    data["budget_cents"] = raw * 100
            except (ValueError, TypeError):
                pass  # Biarkan validasi di level field yang menangani type error-nya

        return super().to_internal_value(data)

Kita akan melakukan test di bagian berikutnya.


Test to_internal_value()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from customization_lab.serializers import FreelanceProjectWriteSerializer

# Payload dengan input berantakan yang akan dinormalisasi to_internal_value
sloppy_payload = {
    "title": "   Fix Login Bug   ",         # ada whitespace tambahan
    "description": "OAuth token expiry issue.",
    "budget_cents": 50,                      # client mengirim 50 maksudnya $50, bukan 50 cents
    "deadline": "2027-09-15",
    "status": "open",
    "client_email": "  ADMIN@ClientCorp.COM  ", # huruf besar/kecil dan whitespace berantakan
}

ser = FreelanceProjectWriteSerializer(data=sloppy_payload)
print(ser.is_valid())

# Cek apa yang dihasilkan to_internal_value sebelum validasi
print(ser.validated_data["title"])         # "Fix Login Bug" (sudah di-strip)
print(ser.validated_data["client_email"])  # "admin@clientcorp.com" (sudah dinormalisasi)
print(ser.validated_data["budget_cents"])  # 5000 (50 * 100, dikonversi ke cents)

Yang perlu diperhatikan:

  • Data yang masuk di-preprocess sebelum field validators manapun melihatnya.
  • Pada saat validated_data tersedia, value-nya sudah bersih.
  • Payload raw milik caller tidak pernah dimutasi (kita memanggil data.copy() terlebih dahulu).

Pastikan to_internal_value tidak mengganggu validasi input yang tidak valid

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from customization_lab.serializers import FreelanceProjectWriteSerializer

# Field wajib yang hilang
missing_required = {
    "description": "No title, no budget, no deadline.",
    "client_email": "someone@test.com",
}

ser2 = FreelanceProjectWriteSerializer(data=missing_required)
print(ser2.is_valid())   # False
print(ser2.errors)       # error jelas untuk title, budget_cents, deadline

# Budget yang bukan integer
bad_budget = {
    "title": "Valid Title Here",
    "budget_cents": "not-a-number",
    "deadline": "2027-06-01",
    "client_email": "test@example.com",
}

ser3 = FreelanceProjectWriteSerializer(data=bad_budget)
print(ser3.is_valid())   # False
print(ser3.errors)       # error di budget_cents

Yang perlu diperhatikan:

  • to_internal_value tidak menekan validation errors. Setelah method ini berjalan, validasi field normal tetap berjalan.
  • Blok except (ValueError, TypeError): pass di serializer membiarkan validasi field menangani type error yang sebenarnya dengan bersih.

Bagaimana keempat tools ini saling bekerja

Keempat tool ini beroperasi di titik yang berbeda dalam pipeline serializer.

Pipeline outgoing (response):

1
2
3
4
5
6
Model instance
-> to_representation(instance)        <-- kode custom kita di sini
   -> menjalankan representation setiap field
      -> SerializerMethodField.get_*  <-- field hasil komputasi ditambahkan di sini
-> mengembalikan dict final
-> JSONRenderer -> HTTP response

Pipeline incoming (request):

1
2
3
4
5
6
7
8
9
request.data (dict mentah)
-> to_internal_value(data)            <-- kode custom kita di sini (masih raw string)
   -> field type parsing
   -> validasi option field (min_length, dll.)
   -> daftar validators=[]
   -> method validate_<field>()
   -> method validate()
-> validated_data (tipe Python yang bersih)
-> save() -> model instance

Panduan visual sederhana untuk memilih tool yang tepat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Butuh value READ-ONLY hasil komputasi di output?
-> SerializerMethodField

Butuh field yang berbeda untuk caller yang berbeda?
-> Custom __init__ (filter self.fields berdasarkan self.context)

Perlu membentuk ulang, rename, atau redact dict output?
-> Override to_representation()

Perlu membersihkan atau menormalisasi raw input sebelum validasi?
-> Override to_internal_value()

Tabel perbandingan keempat tools

ToolBerjalan KapanKegunaan Umum
SerializerMethodFieldJalur outputValue hasil komputasi (harga terformat, boolean turunan, delta yang dihitung)
Custom __init__Sebelum data apapun mengalirMenambah/menghapus field berdasarkan context atau permission caller
to_representationJalur output, setelah semua fieldRename key, mask data sensitif, penghapusan field kondisional
to_internal_valueJalur input, sebelum type parsingMenghapus formatting, normalisasi huruf besar/kecil, precompute value dari raw input

Kesalahan Umum

  • Salah menamai method getter: kalau field-nya days_remaining, method-nya harus get_days_remaining. Typo bisa menyebabkan None secara diam-diam atau AttributeError.
  • Mengakses self.fields atau self.context di __init__ sebelum memanggil super().__init__(). Selalu panggil super terlebih dahulu.
  • Mutate dict data aslinya di to_internal_value. Selalu panggil data = data.copy() sebelum melakukan perubahan. Mutate secara langsung bisa menyebabkan bug yang sulit dilacak lintas retry atau middleware.
  • Lupa memanggil super() di to_representation atau to_internal_value. Tanpa itu, logic field normal tidak akan berjalan sama sekali.
  • Melakukan validasi bisnis di dalam to_internal_value. Itu seharusnya ada di validate_<field> atau validate. to_internal_value hanya untuk preprocessing.
  • Menggunakan to_representation untuk sesuatu yang sebenarnya lebih jelas diekspresikan dengan SerializerMethodField. Pilih tool yang lebih sederhana kalau kita hanya butuh satu field hasil komputasi.
  • Return tipe yang salah dari method get_*: apapun yang kita return harus JSON-serializable (string, int, float, bool, list, dict, atau None).

Poin Penting

TopikYang Perlu Diingat
SerializerMethodFieldField read-only hasil komputasi; method-nya harus bernama get_<nama_field>
Custom __init__Panggil super() dulu; baru modifikasi self.fields berdasarkan self.context
to_representationMenerima model instance; panggil super() untuk dapat dict dasarnya, baru modifikasi
to_internal_valueMenerima dict input mentah; panggil data.copy() sebelum mutate; panggil super() terakhir
Posisi di pipelineto_internal_value berjalan pertama (input); to_representation berjalan terakhir (output)
Passing contextKirim context={“key”: value} saat instantiate serializer dari view

Hampir semua serializer DRF yang akan kita temui pada akhirnya menggunakan setidaknya salah satu dari tool-tool ini. Mengetahui kapan harus menggunakan yang mana, dan kapan sebaiknya tidak, adalah yang membedakan serializer yang mudah di-maintain dengan yang hanya sekedar terlihat bagus.

Foto cover oleh silverkblack dari Unsplash

Dibawah Lisensi CC BY-NC-SA 4.0
Dibangun dengan Hugo
Tema Stack dirancang oleh Jimmy