Featured image of post Field Types dan Validation di Django REST Framework

Field Types dan Validation di Django REST Framework

Penjelasan mengenai Field Types (CharField, IntegerField), Validation di level field dan di level object, beserta contohnya.

Field Types and Validation (CharField, IntegerField, validators, field/object-level validation)

Sebelumnya, kita sudah mempelajari pipeline serializer dan bagaimana ModelSerializer membuat field secara otomatis. Sekarang kita fokus ke kualitas data. API kita tidak hanya harus menerima data, tapi harus menerima data yang benar.

Materi kali ini kita akan belajar bagaimana cara mengontrol input dengan:

  • CharField
  • IntegerField
  • validators
  • validasi di level field (validate_<field_name>)
  • validasi di level object (validate)

Jika kita memahami materi ini, kita bisa menghentikan payload yang tidak valid sebelum data buruk tersebut dimasukkan ke database.


Di Mana Validasi Terjadi dalam Pipeline Serializer

Dari materi sebelumnya, kita sudah tahu alur data masuk (incoming):

1
2
3
4
5
request.data
-> Serializer(data=request.data)
-> serializer.is_valid()
-> serializer.validated_data
-> serializer.save()

Di dalam is_valid(), DRF sebenarnya menerapkan validasi dalam beberapa layer.

1
2
3
4
5
1) Parsing tipe pada field (contoh: "12" -> 12 untuk IntegerField)
2) Validasi option pada field (min_length, max_length, min_value, max_value, required)
3) Daftar validation pada field (validators=[...])
4) Method validate_<field_name>()
5) Method validate() (aturan lintas field, lebih dari satu field sekaligus)

Urutan itu membantu kita memutuskan di mana setiap aturan sebaiknya diletakkan.


Hal Essential Tentang CharField

CharField digunakan untuk input teks pendek seperti title, label, kode, atau nama.

Opsi yang umum digunakan:

  • required=True/False
  • allow_blank=True/False
  • min_length
  • max_length
  • trim_whitespace=True/False

Contoh:

1
2
3
4
5
title = serializers.CharField(
    min_length=5,
    max_length=80,
    trim_whitespace=True,
)

Penjelasan kode ini:

  • input harus berupa teks
  • panjang teks dibatasi
  • spasi di awal dan akhir akan dihapus sebelum validasi ketika trim_whitespace=True

Hal Essential Tentang IntegerField

IntegerField digunakan untuk value berupa bilangan bulat seperti jumlah, limit, umur, dan quantity.

Opsi yang umum digunakan:

  • required=True/False
  • min_value
  • max_value

Contoh:

1
seats = serializers.IntegerField(min_value=5, max_value=100)

Catatan:

  • DRF akan menolak input non-numerik seperti “many”.
  • DRF bisa melakukan parse string numerik seperti “25” menjadi integer 25.

Menggunakan Validators

Kita memiliki beberapa cara untuk melakukan validasi pada field:

  1. Opsi field (min_length, max_value, dll.)
  2. Daftar validators=[...]
  3. Method validate_<field_name>
  4. validate() untuk aturan yang melibatkan beberapa field

Function Custom Validator

Ini dapat digunakan ketika rule ini bisa digunakan berulang kali.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from rest_framework import serializers


def no_reserved_words(value):
    reserved = ["test", "dummy", "sample"]
    lowered = value.lower()
    for word in reserved:
        if word in lowered:
            raise serializers.ValidationError(
                f"Name cannot contain reserved word: {word}"
            )
    return value

Lalu pasangkan ke sebuah field:

1
2
3
4
5
name = serializers.CharField(
    min_length=5,
    max_length=80,
    validators=[no_reserved_words],
)

Validasi di Level Field vs di Level Object

Validasi di Level Field

Gunakan validate_<field_name>(self, value) ketika rules hanya membutuhkan satu field.

1
2
3
4
def validate_seats(self, value):
    if value % 5 != 0:
        raise serializers.ValidationError("Seats must be a multiple of 5.")
    return value

Validasi di Level Object

Gunakan validate(self, attrs) ketika sebuah aturan membutuhkan beberapa field sekaligus.

1
2
3
4
def validate(self, attrs):
    if attrs["min_age"] > attrs["max_age"]:
        raise serializers.ValidationError("min_age cannot be greater than max_age.")
    return attrs

Aturan Mudahnya:

  • Jika hanya satu field -> gunakan validasi field-level
  • Jika membandingkan/menggabungkan beberapa field -> gunakan validasi level object
  • Definisikan function custom validator sebelum serializer class agar bisa dipanggil pada saat class dibuat.

Latihan

Lab ini TIDAK melanjutkan app dari materi serializer sebelumnya. Jangan gunakan ulang app serializer_lab atau modelserializer_lab.

Buat dan daftarkan app baru

Dari root project (my-drf-project):

1
2
source .venv/bin/activate
python manage.py startapp validation_lab

Tambahkan app di config/settings.py:

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

Buat model di validation_lab/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.db import models


class CodingCampSession(models.Model):
    name = models.CharField(max_length=80)
    min_age = models.IntegerField()
    max_age = models.IntegerField()
    seats = models.IntegerField(default=20)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.name

Jalankan migrations

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

Buat serializer di validation_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
55
56
57
58
from rest_framework import serializers
from .models import CodingCampSession


def no_reserved_words(value):
    reserved = ["test", "dummy", "sample"]
    lowered = value.lower()
    for word in reserved:
        if word in lowered:
            raise serializers.ValidationError(
                f"Name cannot contain reserved word: {word}"
            )
    return value


class CodingCampSessionSerializer(serializers.ModelSerializer):
    name = serializers.CharField(
        min_length=5,
        max_length=80,
        trim_whitespace=True,
        validators=[no_reserved_words],
    )
    min_age = serializers.IntegerField(min_value=10, max_value=70)
    max_age = serializers.IntegerField(min_value=10, max_value=70)
    seats = serializers.IntegerField(min_value=5, max_value=100)

    class Meta:
        model = CodingCampSession
        fields = ["id", "name", "min_age", "max_age", "seats", "created_at"]
        read_only_fields = ["id", "created_at"]

    def validate_name(self, value):
        if value[0].isdigit():
            raise serializers.ValidationError("Name cannot start with a number.")
        return value

    def validate_seats(self, value):
        if value % 5 != 0:
            raise serializers.ValidationError("Seats must be a multiple of 5.")
        return value

    def validate(self, attrs):
        min_age = attrs.get("min_age")
        max_age = attrs.get("max_age")

        # Support partial updates where one of the fields may be missing.
        if self.instance:
            if min_age is None:
                min_age = self.instance.min_age
            if max_age is None:
                max_age = self.instance.max_age

        if min_age is not None and max_age is not None and min_age > max_age:
            raise serializers.ValidationError(
                {"non_field_errors": ["min_age cannot be greater than max_age."]}
            )

        return attrs

Test payload yang valid di shell Django

1
python manage.py shell
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from validation_lab.serializers import CodingCampSessionSerializer

valid_payload = {
    "name": "Backend Fundamentals Camp",
    "min_age": 17,
    "max_age": 30,
    "seats": 25,
}

ser = CodingCampSessionSerializer(data=valid_payload)
print(ser.is_valid())
print(ser.errors)

obj = ser.save()
print(obj.id, obj.name, obj.min_age, obj.max_age, obj.seats)

Expected result:

  • is_valid() -> True
  • errors -> {}
  • object is saved with generated id

Test payload invalid satuan

 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
55
56
57
58
59
# 1) Gagal pada validator CharField (mengandung kata yang sudah di-reserved)
bad_name_payload = {
    "name": "Dummy Camp",
    "min_age": 16,
    "max_age": 22,
    "seats": 20,
}

ser1 = CodingCampSessionSerializer(data=bad_name_payload)
print(ser1.is_valid())
print(ser1.errors)

# 2) Gagal pada validasi level Field (validate_seats)
bad_seats_payload = {
    "name": "Python Starter Camp",
    "min_age": 16,
    "max_age": 22,
    "seats": 17,
}

ser2 = CodingCampSessionSerializer(data=bad_seats_payload)
print(ser2.is_valid())
print(ser2.errors)

# 3) Gagal pada validasi level Object
bad_age_range_payload = {
    "name": "API Build Camp",
    "min_age": 30,
    "max_age": 20,
    "seats": 20,
}

ser3 = CodingCampSessionSerializer(data=bad_age_range_payload)
print(ser3.is_valid())
print(ser3.errors)

# 4) Gagal pada konversi tipe IntegerField
bad_integer_payload = {
    "name": "Data Camp",
    "min_age": "young",
    "max_age": 20,
    "seats": 20,
}

ser4 = CodingCampSessionSerializer(data=bad_integer_payload)
print(ser4.is_valid())
print(ser4.errors)

# 5) Beberapa error sekaligus
multi_bad_payload = {
    "name": "Dummy Camp",
    "min_age": 30,
    "max_age": 20,
    "seats": 17,
}

ser5 = CodingCampSessionSerializer(data=multi_bad_payload)
print(ser5.is_valid())
print(ser5.errors)

Hal yang perlu diperhatikan:

  • error dikembalikan per field
  • aturan lintas field muncul di non_field_errors
  • error parsing integer jelas dan API-friendly

Test update secara partial

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from validation_lab.models import CodingCampSession
from validation_lab.serializers import CodingCampSessionSerializer

obj = CodingCampSession.objects.first()

# Partial update yang tidak valid: max_age di bawah min_age yang ada saat ini
patch_ser = CodingCampSessionSerializer(obj, data={"max_age": 12}, partial=True)
print(patch_ser.is_valid())
print(patch_ser.errors)

# Partial update yang valid
ok_patch_ser = CodingCampSessionSerializer(obj, data={"seats": 30}, partial=True)
print(ok_patch_ser.is_valid())
updated = ok_patch_ser.save()
print(updated.seats)

Ini menunjukkan bahwa method validate() kita juga melindungi update secara partial.

Konfirmasi bahwa outgoing serialization masih berfungsi

1
2
3
4
5
6
7
from validation_lab.models import CodingCampSession
from validation_lab.serializers import CodingCampSessionSerializer

sessions = CodingCampSession.objects.all()
ser_many = CodingCampSessionSerializer(sessions, many=True)
print(len(ser_many.data))
print(ser_many.data[0])

Kesalahan Umum

  • Meletakkan logika lintas field di validate_<field> dan bukan di validate
  • Lupa melakukan return value di validate_<field>
  • Lupa melakukan return attrs di validate
  • Hanya membuat pengecekan di frontend dan tidak melakukan validasi serializer di backend
  • Menggunakan aturan validasi yang kompleks sebelum menguasai constraint field yang sederhana
  • Meletakkan validasi di luar serializer daripada di dalam serializer (batas API input)

Poin Penting

  • CharField dan IntegerField adalah guard dari input basic kita untuk teks dan angka.
  • Opsi field (min_length, min_value, dll.) menangani banyak rules umum dengan cepat.
  • validators=[...] berguna untuk rules yang bisa digunakan berkali-kali.
  • validate_<field> digunakan untuk rules pada satu field.
  • validate digunakan untuk rules beberapa field.
  • Validasi serializer yang baik membuat API kita predictable dan database kita tetap rapi.

Foto cover oleh jakubzerdzicki dari Unsplash

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