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:
CharFieldIntegerFieldvalidators- 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/Falseallow_blank=True/Falsemin_lengthmax_lengthtrim_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/Falsemin_valuemax_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:
- Opsi field (
min_length, max_value, dll.) - Daftar
validators=[...] - Method
validate_<field_name> 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
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() -> Trueerrors -> {}- 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