İçeriğe geç

Django Rest Framework Serializer Tips 1

Rest Framework, Django ile geliştirilen sistemlerde API geliştirmeyi keyifli ve acısız kılan harika bir eklenti. Bu yazıda Django Rest Framework’deki belki de en önemli kavram olan serializerları daha efektif ve akılcı kullanabilmek için bazı ileri seviye kullanımlara ve ipuçlarına birlikte göz atacağız. (Örneğin serializerlara extra context paslamak, source keyword, serializer input/output sırasında datayı modifikasyona uğratmak vs)

Hazırsak başlayalım!

Data validasyonu.

DRF serializasyon işlemleri sırasında gelen verinin doğruluğundan emin olmak için bir validasyon mekanizmasına sahiptir ve veriyi kesinlikle validasyondan geçirmenize sizi zorlar. is_valid() metodu çağrıldıktan sonra herhangi bir validasyon hatası olması durumunda ilgili hatalar serializer üstündeki errors attribute’ına bağlanır ve bir ValidationError fırlatılır.

2 tip validasyon bulunur.

  • Field (alan) seviyesinde validasyon.
  • Object (nesne) seviyesinde validasyon.
from django.conf import settings
from django.db import models

class Event(models.Model):
    name = models.CharField(max_length=50)
    is_public = models.BooleanField(default=True)
    start_at = models.DateTimeField()
    end_at = models.DateTimeField()
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    def __str__(self) -> str:
        return self.name

Bu şekilde basit bir modelimiz olduğunu varsayalım ve yukarıda saydığımız validasyon tiplerini örnekler üstünden görelim.

  • İlk validasyon kuralı olarak, kullanıcı yeni bir etkinlik oluştururken geçmişe yönelik bir etkinlik oluşturmasın.
  • İkinci validasyon kuralıysa, etkinlik bitiş tarihi başlangıç tarihinden pekala daha sonra olamasın.

Field (alan) seviyesinde validasyon.

DRF validasyon işlemlerini yaparken serializer üstünde tanımlı olan fieldlara validate_<field_name> metodları tanımlayarak, field üstünde birtakım kontroller yapmanıza olanak verir. Örnekleyecek olursak;

from django.utils import timezone
from django.utils.translation import gettext as _
from rest_framework import serializers

from my_app.models import Event


class EventSerializer(serializers.ModelSerializer):
    class Meta:
        model = Event
        fields = "__all__"

    def validate_start_at(self, value):
        now = timezone.now()
        if now > value:
            raise serializers.ValidationError(
                _("Start date cannot be earlier than today.")
            )
        return value

Böylelikle oluşturulacak her etkinlik için, oluşturulma tarihini bugüne göre kontrol edebiliriz.

Object (nesne) seviyesinde validasyon.

Bazı durumlarda ise oluşturulacak olan validasyon, birden fazla alanın birbiri ile karşılaştırılarak yürütülmesini gerektirebilir. Bu gibi durumlarda validate metodu ezilerek gerekli kontroller sağlanır. Örneğin etkinlik bitiş tarihinin, başlama tarihinden daha sonra olamayacağı gibi;

from django.utils.translation import gettext as _
from rest_framework import serializers

from my_app.models import Event


class EventSerializer(serializers.ModelSerializer):
    class Meta:
        model = Event
        fields = "__all__"


    def validate(self, attrs):
        start_at, end_at = attrs.get("start_at"), attrs.get("end_at")
        if start_at > end_at:
            raise serializers.ValidationError(
                _("Start date must be earlier than end date.")
            )

Fonksiyonel validatorler.

Bir validasyon işleminin birden fazla serializerde yapılması gereken durumlarda, aynı kodu tekrar tekrar yazmaktansa, gerekli validatoru ayrık olarak bir fonksiyon olarak yazabilir ve field üzerinde çalışmasını sağlayabiliriz. Örneğin;

import re

def turkey_phone_number_validator(value):
    if re.match(r"^(05)\d{9}$", value):
        return value
    raise serializers.ValidationError("Phone number format is invalid.")


class ContactUsSerializer(serializers.Serializer):
    phone = serializers.CharField(
        validators=[
            turkey_phone_number_validator,
        ]
    )

Serializer save metoduna ekstra argüman paslamak.

ModelSerializer sınıfının save metodu çağrıldığında, serialize edilen veri ile nesneyi oluşturmakta ya da bir instance serializera geçilmiş ise nesneyi güncellemektedir.

Bazı durumlarda, serialize edilen veriye, doğrudan kullanıcıdan alınmayan fakat güncelleme ya da oluşturma esnasında kullanılacak olan veriyi dahil etme ihtiyacı duyulabilir. Gerçek hayat senaryosu üzerinden konuşacak olursak, istekte bulunan kullanıcıyı oluşturulan yada güncellenen ilgili nesneyle ilişkisel hale getirmek olabilir. Gelen kullanıcıyı doğrudan aşağıdaki şekilde validated_data içerisine dahil edebilirsiniz.

serializer.save(owner=request.user)
serializer.save(requested_ip=request.IP) 

TIPS. Generic viewlar ya da viewsetler kullanıyorsanız ve ilgili view oluşturma ya da güncelleme işlemlerinde bulunuyor ise perform_update, perfom_create ile serializer save metodunu hook altına alarak gerekli veriyi save metoduna paslayabilirsiniz. :thumbsup


HiddenField ve CurrentUserDefault.

Yukarıdaki örnektede göz attığımız üzere, request üzerinde taşınan kullanıcıyı serializer save metodunu hook altına almadan direkt hidden bir field üzerinde belirtebiliriz. HiddenFieldlar serialiazsyon ve deserializasyon işlemleri sırasında kullanılmaz. Doğrudan default keywordi ile istenen data bağlanır.

Örnekleyecek olursak;

class EventSerializer(serializers.ModelSerializer):

    owner = serializers.HiddenField(
        default=serializers.CurrentUserDefault(),
    )

    class Meta:
        model = Event
        fields = "__all__"

Serializasyon/Deserializasyon veri değiştirmek.

BaseSerializer sınıfı to_representation(serializasyon) ve to_internal_value(deserializasyon) adlı 2 metodla serializasyon davranışlarını yönetir. to_representation metodu outputu, to_interval_value ise input sırasında çalışarak (örneğin JSON -> Python primitive types) dönüşümlerini sağlar. Bu iki metodun ezilmesi ile serializasyon/deserializasyon davranışları değiştirilebilir. Örnekleyecek olursak; (Yine event modeli ve serializerini kullanacağız.)

to_representation

Pyhon snake_case bir isimlendirme tercih ederken JavaScript camelCase bir isimlendirme kullanmakta ve çoğu API bu kuralı takip etmekte. to_representation metodunu override ederek nested olmayan nispeten basit bir serializerdan dönecek olan verinin keylerini camelCase yapacağız.

class EventSerializer(serializers.ModelSerializer):

    class Meta:
        model = Event
        fields = "__all__"

    def to_representation(self, instance):
        instance_data = super().to_representation(instance)
        default_keys = instance_data.keys()
        camel_cased_instance_data = {}
        for key in default_keys:
            words = key.split("_")
            camel_cased_key = words[0] + "".join(
                word.title() for word in words[1:] if len(words) > 1
            )
            camel_cased_instance_data[camel_cased_key] = instance_data[key]

        return camel_cased_instance_data

Serialize edilen nesnenin serializasyon sonucu dönen (dict type) verisi üzerinde biraz Pythonic işlem yaparak dinamik bir yeni dict oluşturduk. Çıktısına göz atalım;

$ http --body :4000/api/events/1/
{
    "endAt": "2021-07-10T10:42:42.790973Z",
    "id": 1,
    "isPublic": false,
    "name": "lorem ipsum.",
    "owner": 1,
    "startAt": "2021-07-10T10:42:42.790973Z"
}

Not. Bu kullanım nested olmayan serializerlarda başarılıyla çalışacakken, nested ve birden fazla yerde camelCase dönüşümü yapılması gereken kompleks senaryolarda kullanışsız olacaktır. Neyse ki bu ihtiyacınızı karşılayacak ve doğrudan Renderer sınıfları üzerinden bu süreci ele alan paketi buradaki linkten inceleyebilirsiniz.

 

to_internal_value

Ufak bir senaryo düşünelim. Kullanıcıların oluşturduğu etkinlikler dışında kullandığımız 3rd party servislere entegrasyon sağlayarak, otomatik olarak etkinlikler oluşturuyor ve sistemi besliyoruz. Fakat 3rd party servisin etkinlik entegrasyonu sırasında dönmüş olduğu veri kompleks ve ayıklanması gerekiyor. Örnekleyecek olursak;

{
    "is_success": true,
    "status_code": 200,
    "community": {
        "name": "complex data is here"
    },
    "event": {
        "name": "3rd party!",
        "start_at": "2021-07-10T10:42:42.790973Z",
        "end_at": "2021-07-10T10:41:42.790973Z",
        "owner": 1,
        "is_public": true
    }
}

Gelen raw data içersindeki event keyine karşılık gelen veriyi ayıkladıktan sonra, Python primitive typelara çevrilierek işlenebilmesi için to_internal_value metoduna teslim ediyoruz.

class EventSerializer(serializers.ModelSerializer):
    class Meta:
        model = Event
        fields = "__all__"


    def to_internal_value(self, data):
        3rd_part_data = data["event"]
        return super().to_internal_value(data=3rd_part_data)

Artık dış servisten dönen veriyi serialize ettikten sonra içeriye kabul edebiliriz.


extra_kwargs.

Bazı durumlarda, serializer üstünde tanımlı olan fiedları yeniden tanımlamadan, field özelinde argümanlar geçmek isteyebiliriz. Bir fielda gereklilik eklemek, maksimum uzunluk belirlemek veya sadece yazım yani deserializasyon sırasında kullanılabileceği işaretlemek vs. Örnekleyecek olursak;

from django.contrib.auth import get_user_model


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        fields = "__all__"
        model = get_user_model()
        extra_kwargs = {
            "password": {"write_only": True},
            "first_name": {"required": True},
            "last_name": {"required": True},
            "date_joined": {"read_only": True}
        }

extra_kwargs Meta sınıfta tanımlanan bir alan. Bu dicte yukarıdaki gibi serializer üstünde bulunan fieldlar icin attribute tanımlamaları yaparak, fieldin serializasyon sırasındaki davranışını, tekrar tanımlamadan değiştirebilmek mümkün.


Extra context.

Bazı durumlarda, serializer içinde kullanılması için context üzerinde ekstra veri sağlamak isteyebilirsiniz. Örneğin, gelen request context içinde gönderilir ise, serialize edilen nesnenin ilişkili olduğu diğer nesnelerin absolute urlleri oluşturulabilir, veya gelen contexte göre to_representation metodunda bir özelleştirme yapabilirsiniz.

ProfileSerializer(profile, context= {"request":request})

ProfileSerializer(profile, context= {"request":request, "key":"value"})
class ProfileSerializer(serializers.ModelSerializer):
    
    def to_representation(self, instance):
        instance_data = super().to_representation(instance=instance)
        if self.context.get("key") == "value":
            # do stuff

        return instance_data

Sonuç.

Bu yazı serisinin ilkinde DRF serializerlar ile çalışırken işlerimizi hızlandırıp, kolaylaştıracak bir kaç ipucu ve advanced kullanıma göz attık. Farklı ihtiyaçlara göre nereyi özelleştirmemiz veya neyi ne şekilde kullanmamız gerektiğini artık biliyoruz.

Yazının okunabilirliğini düşürmemek adına, depth, source keywordleri, SerializerMethodField ve okuma/yazma için ayrı serializer vs. kullanımlarını bu yazıya dahil etmedim. Bu kullanımları ve bir kaç farklı konuyuda örnekleyeceğimiz 2. bir yazı yazacağım.

Daha detaylı bilgi için DRF serializer api doc burada.

Görüşmek üzere!

 

 

Tarih:Blog

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.

Göster
Gizle