스터디/Web

[Django] DRF : Serializer 기본 사용법, 유효성 검증

sollllllll 2022. 1. 30. 23:28

참고 : https://www.django-rest-framework.org/api-guide/serializers/

 

 

장고 레스트 프레임워크(Django REST Framework)를 이용해 REST API 환경을 구축하다보면

Serializer를 거의 필수적으로 사용하게 된다.

하지만 Serializer가 어떤 개념인지 잘 와닿지 않아서 Serializer에 대해 공부하고 알게 된 것들을 정리해보려고 한다.

 

 

 

(1) Serializer란?

 

쿼리셋이나 모델 인스턴스 같은 복잡한 데이터들을 네이티브 파이썬 데이터 타입으로 변환하여

json, xml 등의 컨텐츠 타입으로 쉽게 렌더링할 수 있게 한다.

데이터를 직렬화하는 Serializer는 역직렬화 기능 또한 제공하기 때문에

들어오는 데이터를 검증한 후에 분석된 데이터를 복잡한 형식으로 다시 변환할 수도 있다.

REST Framework의 Serializer는 장고의 Form, ModelForm 클래스와 매우 유사하게 작동한다.

DRF가 제공하는 Serializer 클래스는 응답의 출력을 편리하게 관리할 수 있고,

ModelSerializer는 모델 인스턴스 및 쿼리 집합을 처리하는 직렬화 프로그램을 간편하게 만들 수 있도록 한다.

 

 

 

 

(2) Serializer 선언하고 모델 직렬화/역직렬화 하기

 

다음과 같은 모델을 선언했다고 가정하자.

(예시로 사용하는 모든 코드의 출처는 공식 사이트이다.)

# models.py
from datetime import datetime

class Comment:
    def __init__(self, email, content, created=None):
        self.email = email
        self.content = content
        self.created = created or datetime.now()

comment = Comment(email='leila@example.com', content='foo bar')

다음으로는 해당 모델을 직렬화해서 사용할 수 있도록 Serializer를 선언한다.

# serializer.py
from rest_framework import serializers

class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

 

 

이제 CommentSerializer(이하 CS)를 사용하여 Comment 객체와 Comment 리스트를 직렬화할 수 있다.

serializer = CommentSerializer(comment)
serializer.data
# 데이터가 딕셔너리 타입으로 반환된다.

다음 코드를 작성해 모델 인스턴스를 파이썬 네이티브 데이터 유형으로 변환했다.

직렬화 프로세스를 마무리하기 위해서 데이터를 json으로 렌더링한다.

from rest_framework.renderers import JSONRenderer

json = JSONRenderer().render(serializer.data)
json
# b'{"email":"leila@example.com","content":"foo bar","created":"2016-01-27T15:17:10.375877"}'

 


역직렬화도 비슷하게 구현할 수 있다. 먼저, stream을 파이썬 네이티브 데이터 타입으로 구문 분석한다.

import io
from rest_framework.parsers import JSONParser

stream = io.BytesIO(json)
data = JSONParser().parse(stream)

 

다음으로, 이런 기본 데이터 타입을 검증된 데이터 딕셔너리로 변환한다.

serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# 데이터가 딕셔너리 타입으로 반환됨

 

 

 

(3) 인스턴스 저장하기

검증된 데이터를 기반으로 완전한 개체 인스턴스를 반환하려면

create() 및 update() 메소드를 하나 또는 둘 다 구현해야 한다.

# serializer.py
class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

    def create(self, validated_data):
        return Comment(**validated_data)

    def update(self, instance, validated_data):
        instance.email = validated_data.get('email', instance.email)
        instance.content = validated_data.get('content', instance.content)
        instance.created = validated_data.get('created', instance.created)
        return instance

객체 인스턴스가 Django 모델과 일치할 경우, 구현한 메소드가 객체를 데이터베이스에 저장하게 할 수도 있다.

# serializer.py
def create(self, validated_data):
	return Comment.objects.create(**validated_data)

def update(self, instance, validated_data):
	instance.email = validated_data.get('email', instance.email)
	instance.content = validated_data.get('content', instance.content)
	instance.created = validated_data.get('created', instance.created)
	instance.save()
	return instance

이 과정을 거치면 데이터를 역직렬화할 때 save() 메소드를 호출하여 객체 인스턴스를 반환할 수 있습니다.

(단, 데이터가 검증된 상태일 때)

save() 메소드를 호출하면 Serializer 클래스를 인스턴스화 할 때 기존 인스턴스가 전달된 것인지의 여부에 따라

새 인스턴스가 생성되거나 기존 인스턴스가 업데이트된다.

comment = serializer.save()

# 기존에 없는 인스턴스일 때: save() will create a new instance.
serializer = CommentSerializer(data=data)

# 기존에 있는 인스턴스일 때: save() will update the existing `comment` instance.
serializer = CommentSerializer(comment, data=data)

다만 create()와 update() 메소드는 선택사항으로, Serializer 클래스를 어떻게 사용할 것인지에 따라서

하나 또는 둘 다 구현할 수도, 구현하지 않을 수도 있다.

 

 

save() 메소드에 추가 특성을 전달할 수도 있는데, 경우에 따라 인스턴스를 저장할 때 추가 데이터를 주입할 수 있다.
예를 들어 이 추가하는 데이터에는 현재 사용자, 현재 시간 또는 요청 데이터가 아닌 다른 정보가 포함될 수 있다.

사용 방법은 다음 코드처럼 save() 메소드를 호출할 때 추가 키워드 인수를 포함하면 된다.

추가한 인수는 create() 또는 update() 메소드가 호출될 때 validated_data 인수에 포함된다.

(위에 작성한 create, update 메소드 참고)

serializer.save(owner=request.user)

 

 

구현하려는 기능의 경우에 따라 새 인스턴스를 만들지 않고 메일이나 다른 메세지를 보내는 등 

create()나 update() 메소드의 이름이 의미가 없을 수 있다.

그럴 땐 save() 메소드를 직접 오버라이딩 할 수도 있다.
이런 경우에는 Serializer의 validated_data 속성에 직접 액세스해야 한다.

class ContactForm(serializers.Serializer):
    email = serializers.EmailField()
    message = serializers.CharField()

    def save(self):
        email = self.validated_data['email']
        message = self.validated_data['message']
        send_email(from=email, message=message)

 

 

 

(4) 유효성 검증하기 (Validation)

 

데이터를 역질렬화 할 때, 검증된 데이터에 액세스하거나 인스턴스를 저장하기 전에는 

항상 is_valid() 메소드를 호출해야 한다.
만약 검증 과정에서 오류가 발생하면 error 속성에 에러 메세지를 나타내는 딕셔너리가 포함된다. (저장된다.)
딕셔너리의 각 키는 필드 이름이 되고, 그 값은 해당하는 필드에 해당하는 오류 메세지가 된다. 

non_field_errors 키도 있을 수 있고, 이 경우는 일반적인 검증 오류 목록이 된다.
객체 리스트를 역직렬화 할 때, 오류는 각 항목을 나타내는 딕셔너리 목록으로 반환된다.

serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}

 

 

또한 is_valid() 메소드는 raise_exception 플래그를 사용하여 검증 오류가 발생했을 때

serializers.ValidationError 예외를 발생시킨다.
이런 예외들은 HTTP 400 반응을 반환하는 REST 프레임워크 제공의 기본 예외 핸들러를 통해 자동으로 처리된다.

# 데이터가 검증되지 않으면 400 응답을 반환한다.
serializer.is_valid(raise_exception=True)

 

 

Serializer 하위 클래스에 validate_<field_name>() 메소드를 추가해서 

사용자 지정의 필드 수준 검증을 지정할 수 있다. 

이는 장고 Form의 clean_[field_name]() 메소드와도 비슷하며, 

검증이 필요한 필드 값를 인수로 하는 단일 인수를 사용한다.

이 메소드는 검증된 값을 반환하거나 serializers.ValidationError를 발생시켜야 한다.

from rest_framework import serializers

class BlogPostSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=100)
    content = serializers.CharField()

    def validate_title(self, value):
        """
        Check that the blog post is about Django.
        """
        if 'django' not in value.lower():
            raise serializers.ValidationError("Blog post is not about Django")
        return value

 

 

여러 필드에 액세스해야 하는 다른 객체 수준의 유효성 검사를 수행하기 위해서

Serializer 하위 클래스에 validate() 라는 메소드를 추가한다. 
이 메소드는 필드 값의 딕셔너리 인수를 단일로 사용하며,

필드 수준과 마찬가지로 검증된 값을 반환하거나 필요한 경우엔 serializers.ValidationError를 발생시켜야 한다.

from rest_framework import serializers

class EventSerializer(serializers.Serializer):
    description = serializers.CharField(max_length=100)
    start = serializers.DateTimeField()
    finish = serializers.DateTimeField()

    def validate(self, data):
        """
        Check that start is before finish.
        """
        if data['start'] > data['finish']:
            raise serializers.ValidationError("finish must occur after start")
        return data

 

 

Serializer의 개별 필드는 필드 인스턴스에 선언하여 검증자(Validator)를 포함할 수 있다. 

def multiple_of_ten(value):
    if value % 10 != 0:
        raise serializers.ValidationError('Not a multiple of ten')

class GameRecord(serializers.Serializer):
    score = IntegerField(validators=[multiple_of_ten])
    ...

Serializer 클래스에는 전체 필드 데이터 집합에 적용되는 재사용 가능한 검증자가 포함될 수 있으며,

이런 검증자는 다음과 같이 내부 메타 클래스에 선언함으로써 포함된다.

class EventSerializer(serializers.Serializer):
    name = serializers.CharField()
    room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
    date = serializers.DateField()

    class Meta:
        # Each room only has one event per day.
        validators = [
            UniqueTogetherValidator(
                queryset=Event.objects.all(),
                fields=['room_number', 'date']
            )
        ]

 

 

 

이번 포스트에서는 Serializer의 기본적인 사용법과 유효성을 검사하는 방법을 알아보았다.

더 자세하고 다양한 내용들이 많지만, 기본적으로 프로젝트에 적용할 수 있는 개념은 이 정도인 것 같다.

오늘 스터디 한 내용들을 바탕으로 프로젝트에서 Serializer를 잘 사용해 볼 수 있을 것 같다.