본문 바로가기
Django/Django REST framework

9-1. Serializers

by hyun-am 2021. 7. 15.

Serializers

serializer의 유용성을 확장하는 것은 우리가 다루고 싶은 것입니다. 그러나 사소한 문제는 아니며 심각한 디자인 작업이 필요합니다.

Serializers를 사용하면 쿼리 셋 및 모델 인스턴스와 같은 복잡한 데이터를 JSON, XML 또는 기타 콘텐츠 유형으로 쉽게 렌더링 할 수 있는 네이티브한 파이썬 데이터 타입으로 변환할 수 있습니다. Serializer는 또한 deserialization을 제공하여 들어오는 데이터를 먼저 확인한 후 구문 분석 된 데이터를 복잡한 형식으로 다시 변환할 수 있습니다.

DRF의 serializer는 Django의 Form 및 ModelForm 클래스와 매우 유사하게 작동합니다. 응답 출력을 제어하는 강력하고 일반적인 방법을 제공하는 Serializer 클래스와 모델 인스턴스 및 쿼리 셋을 처리하는 Serializer를 만드는 데 유용한 shortcut을 제공하는 ModelSerializer 클래스를 제공합니다.

Serializers 선언하기

예제 목적으로 사용할 수 있는 간단한 객체를 생성하는 것으로 시작하겠습니다.

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')

Comment객체에 해당하는 데이터를 serialize 및 deserialize하는 데 사용할 수 있는 serializer를 선언합니다.

serializer를 선언하는 것은 form을 선언하는 것과 매우 유사합니다.

from rest_framework import serializers

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

Serializing objects

이제 CommentSerializer를 사용하여 comment 또는 comment list를 serializer할 수 있습니다. 다시 말하지만, Serializer클래스를 사용하는 것과 매우 유사합니다.

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

이 시점에서 모델 인스턴스를 Python 기본 데이터 타입으로 변환했습니다. 시리얼라이저 프로세스를 마무리하기 위해 데이터를 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"}'

Deserializing objects

역 직렬화도 비슷합니다. 먼저 스트림을 Python 기본 데이터 타입으로 parse합니다.

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
# {'content': 'foo bar', 'email': 'leila@example.com', 'created': datetime.datetime(2012, 08, 22, 16, 20, 09, 822243)}

Saving instances

검증 된 데이터를 기반으로 완전한 객체 인스턴스를 반환할 수 있으려면 .create() 및 .update()메서드 중 하나 또는 둘 다 구현해야합니다.

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 모델에 해당하는 경우 이러한 메서드가 데이터베이스에 저장하도록해야합니다. 예를 들어 Comment가 Django 모델인 경우 메서드는 다음과 같습니다.

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()를 호출하여 검증된 데이터를 기반으로 객체 인스턴스를 반환할 수 있습니다.

comment = serializer.save()

.save()를 호출하면 serializer 클래스를 인스턴스화 할 때 기존 인스턴스가 전달되었는지 여부에 따라 새 인스턴스가 생성되거나 기존 인스턴스가 업데이트됩니다.

# .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()에 추가 속성 전달

인스턴스를 저장할 때 view 코드가 추가 데이터를 삽입할 수 있도록 하는 경우가 있습니다. 이 추가 데이터에는 현재 사용자, 현재 시간 또는 요청 데이터의 일부가 아닌 기타 정보가 포함될 수 있습니다.

.save()를 호출할 때 추가 키워드 아규먼트를 포함하면 됩니다. 예를 들면 아래와 같습니다.

serializer.save(owner=request.user)

추가 키워드 아규먼트는 .create() 또는 .update()가 호출될 때 validated_data 인수에 포함됩니다.

.Save() 재정의

경우에 따라 .create() 및 .update()메서드 이름이 의미가 없을 수 있습니다. 예를 들어, 연락처 양식에서는 새 인스턴스를 생성하지 않고 대신 이메일 또는 기타 메시지를 보낼 수 있습니다.

이러한 경우 더 읽기 쉽고 의미가 있으므로 .save()를 직접 재 정의하도록 할 수 있습니다. 예시는 다음과 같습니다.

 

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)

위의 경우 이제 serializer .validated_data 속성에 직접 액세스해야합니다.

Validation

데이터를 역 직렬화 할 때 유효성이 검사 된 데이터에 액세스하거나 개체 인스턴스를 저장하기 전에 항상 is_valid()를 호출해야합니다. 유효성 검사 오류가 발생하면 .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.']}

사전의 각 키는 필드 이름이 되고, 값은 해당 필드에 해당하는 오류 메시지의 문자열 목록이 됩니다.

non_field_errors 키도 있을 수 있으며 일반적인 유효성 검사 오류를 나열합니다. non_field_errors 키의 이름은 NON_FIELD_ERRORS_KEY 프레임워크 설정을 사용하여 사용자 지정할 수 있습니다.

아이템 리스트를 역 직렬화 할 때 역 직렬화 된 각 아이템을 나타내는 딕셔너리 목록으로 오류가 반환됩니다.

Raising an exception on invalid data(유효하지 않은 데이터에 대한 예외 발생)

.is_valid() 메서드는 유효성 검사 오류가 있는 경우 serializers.ValidationError 예외를 발생시키는 선택적 raise_exception 플래그를 사용합니다.

이러한 예외는 REST 프레임워크에서 제공하는 기본 예외 처리기에 의해 자동으로 처리되며 기본적으로 HTTP 400 Bad Request 응답을 반환합니다.

# Return a 400 response if the data was invalid.
serializer.is_valid(raise_exception=True)

Field-level validation

Serializer 하위 클래스에 .validate_<field_name> 메서드를 추가하여 사용자 지정 필드 수준 유효성 검사를 지정할 수 있습니다. 이는 Django 양식의 .clean_<field_name> 메소드와 유사합니다.

이러한 메서드는 유효성 검사가 필요한 필드 값인 단일 아규먼트를 사용합니다.

validate_<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

참고 : <field_name>이 required = False 매개 변수를 사용하여 serializer에서 선언 된 경우 필드가 포함되지 않은 경우 이 유효성 검사 단계가 수행되지 않습니다.

Object-level validation

여러 필드에 엑세스해야하는 다른 유효성 검사를 수행하려면 Serializer 하위 클래스에 .validate()라는 메서드를 추가합니다. 이 메서드는 필드 값의 딕셔너리인 단일 아규먼트를 사용합니다. 필요한 경우 raise에 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

Validators

serializer의 개별 필드는 필드 인스턴스에서 선언하여 벨리데이터를 포함할 수 있습니다. 예를 들면 다음과 같습니다.

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']
            )
        ]

더 많은 정보는 https://www.django-rest-framework.org/api-guide/validators/

 

Validators - Django REST framework

 

www.django-rest-framework.org

Accessing the initial data and instance

초기 개체 또는 쿼리셋을 serializer 인스턴스에 전달할 때 개체를 .instance로 사용할 수 있습니다. 초기 객체가 전달되지 않으면, .instance 속성은 None이 됩니다.

serializer 인스턴스에 데이터를 전달할 때 수정되지 않은 데이터는 .initial_data로 사용할 수 있습니다. 데이터 키워드 인수가 전달되지 않으면 .initial_data로 사용할 수 있습니다. 데이터 키워드 아규먼트가 전달되지 않으면 .initial_data 속성이 존재하지 않습니다.

Partial updates

기본적으로 serializer는 모든 필수 필드의 값을 전달해야 합니다. 그렇지 않으면 벨리데이터 오류가 발생합니다. 부분적인 업데이트를 허용하기 위해 부분 아규먼트를 사용할 수 있습니다.

# Update `comment` with partial data
serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True)

Dealing with nested objects

이전 예제는 단순한 데이터 유형 만 있는 객체를 처리하는 데 적합하지만 때로는 더 복잡한 객체를 나타낼 수 있어야 합니다. 여기서 객체의 일부 속성은 문자열, 날짜 또는 정수와 같은 단순한 데이터 유형이 아닐 수 있습니다.

Serializer 클래스는 그 자체가 Field의 유형이며 한 개체 유형이 다른 개체 유형 내에 중첩되는 관계를 나타내는 데 사용할 수 있습니다.

class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField(max_length=100)

class CommentSerializer(serializers.Serializer):
    user = UserSerializer()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

중첩 된 표현이 선택적으로 None 값을 받아 들일 수 있다면, 중첩 된 serializer에 required = False 플래그를 전달해야합니다.

class CommentSerializer(serializers.Serializer):
    user = UserSerializer(required=False)  # May be an anonymous user.
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

마찬가지로 중첩 표현이 아이템 리스트여야 하는 경우 중첩된 serializer에 many=True 플래그를 전달해야 합니다.

class CommentSerializer(serializers.Serializer):
    user = UserSerializer(required=False)
    edits = EditItemSerializer(many=True)  # A nested list of 'edit' items.
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

Writable nested representations

데이터 역 직렬화를 지원하는 중첩된 표현을 처리 할 때 중첩 된 개체의 오류는 중첩된 개체의 필드 이름 아래에 중첩됩니다.

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

마찬가지로 .validated_data 속성에는 중첩된 데이터 구조가 포함됩니다.

Writing .create() methods for nested representations

쓰기 가능한 중첩 표현을 지원하는 경우 여러 객체 저장을 처리하는 .create() 또는 .update() 메서드를 작성해야 합니다.

다음 예제는 중첩 된 프로필 개체를 사용하여 사용자 생성을 처리하는 방법을 보여주는 코드입니다.

class UserSerializer(serializers.ModelSerializer):
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = ['username', 'email', 'profile']

    def create(self, validated_data):
        profile_data = validated_data.pop('profile')
        user = User.objects.create(**validated_data)
        Profile.objects.create(user=user, **profile_data)
        return user

Writing .update() methods for nested representations

업데이트의 경우 관계 업데이트를 처리하는 방법에 대해 신중하게 생각해야 합니다. 예를 들어 관계에 대한 데이터가 None이거나 제공되지 않은 경우 다음과 같은것이 발생합니다.

  • 데이터베이스에서 관계를 Null로 설정해야합니다.
  • 연관된 인스턴스를 삭제합니다.
  • 데이터를 무시하고 인스턴스를 그대로 둡니다.
  • 벨리데이션 에러를 발생시킵니다.

다음은 이전 UserSerializer 클래스의 .update() 메서드에 대한 예시입니다.

def update(self, instance, validated_data):
        profile_data = validated_data.pop('profile')
        # Unless the application properly enforces that this field is
        # always set, the following could raise a `DoesNotExist`, which
        # would need to be handled.
        profile = instance.profile

        instance.username = validated_data.get('username', instance.username)
        instance.email = validated_data.get('email', instance.email)
        instance.save()

        profile.is_premium_member = profile_data.get(
            'is_premium_member',
            profile.is_premium_member
        )
        profile.has_support_contract = profile_data.get(
            'has_support_contract',
            profile.has_support_contract
         )
        profile.save()

        return instance

중첩 된 create 및 update의 동작이 모호 할 수 있고 관련 모델간에 복잡한 종속성이 필요할 수 있으므로 REST 프레임 워크 3에서는 항상 이러한 메서드를 명시 적으로 작성해야 합니다. 기본 ModelSerializer .create() 및 .update() 메서드는 쓰기 가능한 중첩 표현에 대한 지원을 포함하지 않습니다.

그러나 자동 쓰기 가능 중첩 표현을 지원하는 DRF Writable Nested와 같은 써드파티 패키지를 사용할 수 있습니다.

Handling saving related instances in model manager classes

serializer에 여러 관련 인스턴스를 저장하는 대안은 올바른 인스턴스 생성을 처리하는 사용자 지정 모델 관리자 Manager 클래스를 작성하는 것입니다.

예를 들어 사용자 인스턴스와 프로필 인스턴스가 항상 한쌍으로 함께 생성되도록하고 싶다고 가정해 보겠습니다. 다음과 같은 사용자 지정 manager클래스를 작성할 수 있습니다.

class UserManager(models.Manager):
    ...

    def create(self, username, email, is_premium_member=False, has_support_contract=False):
        user = User(username=username, email=email)
        user.save()
        profile = Profile(
            user=user,
            is_premium_member=is_premium_member,
            has_support_contract=has_support_contract
        )
        profile.save()
        return user

이 manager 클래스는 이제 사용자 인스턴스와 프로파일 인스턴스가 항상 동시에 작성된다는 것을 더욱 멋지게 캡슐화 합니다. 이제 serializer 클래스의 .create() 메서드를 다시 작성하여 새 관리자 메서드를 사용할 수 있습니다.

Dealing with multiple objects

Serializer 클래스는 개체 목록의 직렬화 또는 역 직렬화도 처리할 수 있습니다.

Serializing multiple objects

단일 개체 인스턴스 대신 쿼리셋 또는 개체 목록을 직렬화하려면 serializer를 인스턴스화 할 때 Many= True 플래그를 전달해야합니다. 그런 다음 직렬화 할 쿼리셋 또는 개체 리스트를 전달할 수 있습니다.

 

queryset = Book.objects.all()
serializer = BookSerializer(queryset, many=True)
serializer.data
# [
#     {'id': 0, 'title': 'The electric kool-aid acid test', 'author': 'Tom Wolfe'},
#     {'id': 1, 'title': 'If this is a man', 'author': 'Primo Levi'},
#     {'id': 2, 'title': 'The wind-up bird chronicle', 'author': 'Haruki Murakami'}
# ]

Deserializing multiple objects

여러 개체를 역 직렬화하는 기본 동작은 multi object creation을 지원하지만 multi object update는 지원하지 않는 것입니다. 이러한 경우를 지원하거나 사용자 지정하는 방법에 대한 자세한 내용은 아래 ListSerializer 설명서를 참조하면 되겠습니다.

Including extra context

직렬화되는 개체 외에도 serializer 추가 컨텍스트를 제공해야하는 경우가 있습니다. 한 가지 일반적인 경우는 하이퍼링크 된 관계를 포함하는 serializer를 사용하는 경우입니다. serializer가 현재 요청에 액세스하여 정규화된 URL을 제대로 생성할 수 있어야 합니다.

serializer를 인스턴스화 할 때 컨텍스트 아규먼트를 전달하여 임의의 추가 컨텍스트를 제공할 수 있습니다. 예를들면 다음과 같습니다.

serializer = AccountSerializer(account, context={'request': request})
serializer.data
# {'id': 6, 'owner': 'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}

컨텍스트 딕셔너리는 self.context 속성에 액세스하여 사용자 정의 .to_representation() 메소드와 같은 시리얼라이저 필드 로직 내에서 사용할 수 있습니다.

 

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

 

Serializers - Django REST framework

 

www.django-rest-framework.org

 

'Django > Django REST framework' 카테고리의 다른 글

9-3 Serializer-3  (0) 2021.07.15
9-2 Serializers-2  (0) 2021.07.15
8. Renderers  (0) 2021.07.07
7. Parsers  (0) 2021.06.29
6. Routers  (0) 2021.06.28

댓글