본문으로 바로가기

Django Restframework Serializer 뜯어보기

category Coding/Python 2019.09.19 12:28

현재 Custom Authentication을 구현하여 request.user에 Header에 들어있는 특정한 값을 파싱하여 넣도록 구현했다. 그 다음으로 리퀘스트를 검증하기위해 Serializer를 사용하는데, 일반적으로 시리얼라이저는 정의해놓은 필드이외의 값을 받지 못한다. 예를 들어 아래와 같은 시리얼라이저가 있다고 가정해보자.

class CreatePostRequestSerializer(serializers.Serializer):
    body = serializers.CharField(required=False)
    attachments = serializers.ListField(required=False)

리퀘스트로는 body, attachments 두개의 값이 들어오고 제대로 값이 들어왔는지 판단하는 시리얼라이저다. 이 부분에서 is_valid() 이후 serializer.data를 찍으면 request.user에 들어있는 유저의 아이디값을 같이 찍어주고 싶었다. 다음과 같은 검증되지 않은 플로우를 생각했다.

  1. 시리얼라이저에 리퀘스트를 대입할 때 request.data등의 값이 아닌 request객체 자체를 넘긴다.
  2. 검증할 때 클래스 내부 변수를 생성한다음 request.user의 값으로 채워주고 검증 대상은 request.data로 교체한다.
  3. 최종적으로 .data를 통해 검증된 값을 출력할 때 2번에서 생성한 클래스 내부 변수를 포함하여 리턴한다.

좀 찾아보니 CreatePostRequestSerializer(data=리퀘스트)와 같은 형식으로 시리얼라이저에 값을 삽입하면 to_internal_value()라는 함수가 먼저 호출된다. 따라서 시리얼라이저 내부에 해당 함수를 오버라이딩한다음 원하는 내용을 넣어주면 된다.

  def to_internal_value(self, data):
        from collections.abc import Mapping
        from rest_framework.exceptions import ValidationError
        from rest_framework.settings import api_settings
        from collections import OrderedDict
        from django.core.exceptions import \
            ValidationError as DjangoValidationError
        from rest_framework.fields import get_error_detail, set_value
        from rest_framework.fields import SkipField
        from rest_framework.request import Request

          """
          Dict of native values <- Dict of primitive datatypes.
          """
        if isinstance(data, Request):
            setattr(self, 'user_id', int(data.user))
            data = data.data
        if not isinstance(data, Mapping):
            message = self.error_messages['invalid'].format(
                datatype=type(data).__name__
            )
            raise ValidationError({
                api_settings.NON_FIELD_ERRORS_KEY: [message]
            }, code='invalid')

        ret = OrderedDict()
        errors = OrderedDict()
        fields = self._writable_fields

        for field in fields:
            validate_method = getattr(self, 'validate_' + field.field_name,
                                      None)
            primitive_value = field.get_value(data)
            try:
                validated_value = field.run_validation(primitive_value)
                if validate_method is not None:
                    validated_value = validate_method(validated_value)
            except ValidationError as exc:
                errors[field.field_name] = exc.detail
            except DjangoValidationError as exc:
                errors[field.field_name] = get_error_detail(exc)
            except SkipField:
                pass
            else:
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret

모든 코드는 기존 함수의 코드와 동일하고 if isinstance(data, Request)를 포함한 아래 2줄만 추가한 부분이다. 범용적으로 사용하기위해 입력값이 Request객체 자체일때는 user_id라는 변수에 request.user의 값을 넣어주고(해당 함수에서는 request가 data라는 네이밍으로 넘어오므로 data.user) 실제로 Validation을 진행하는 변수는 data.data(마찬가지로 실제로는 request.data)값을 대입해줬다.

마지막으로 data()함수로 오버라이딩을 해줘야한다.

    @property
    def data(self):
        from rest_framework.utils.serializer_helpers import ReturnDict
        ret = super().data
        ret['user_id'] = self.user_id
        return ReturnDict(ret, serializer=self)

ret에 최종적으로 리턴될 데이터가 들어있으므로 위에서 생성하고 값을 넣어줬던 user_id변수를 ret 딕셔너리에 포함시킨 후 리턴해준다.

조금 어거지로 변형한 것 같긴한데, 나름 목적을 이뤘으니 실제 프로덕트에 적용할지는 앞으로 생각하면 될 것 같다.



다른 사람들이 많이 읽은 글

댓글을 달아 주세요