Django REST Framework/DRF 일반

[DRF] 공식 문서 - validators 정리

bluebamus 2024. 1. 16.

 1. Validators

   1) 정의

      - REST 프레임워크 에서 대부분의 경우 기본 필드 유효성 검사를 의존하거나 serializer나 필드 클래스에 명시적인 유효성 검사 메서드를 작성하는 것으로 유효성 검사를 처리한다.

      - 하지만 때로는 유효성 검사 로직을 재사용 가능한 구성 요소에 배치하여 코드 베이스 전체에서 쉽게 재사용할 수 있도록 할 수도 있다. 이를 위해 유효성 검사기 함수와 유효성 검사기 클래스를 사용할 수 있다.

 

   2) Validation in REST framework

      - Django REST 프레임워크 시리얼라이저에서의 유효성 검사는 Django의 ModelForm 클래스에서의 유효성 검사와 조금 다르게 처리된다.

 

      - ModelForm의 경우, 유효성 검사는 일부는 폼에서 수행되고 일부는 모델 인스턴스에서 수행된다. REST 프레임워크에서는 유효성 검사가 전적으로 시리얼라이저 클래스에서 수행된다. 이는 다음과 같은 이점이 있다

         - 코드 동작이 명확해져서 코드의 동작이 명확하게 드러난다.

         - ModelSerializer 클래스와 명시적인 Serializer 클래스 간의 전환을 쉽게 할 수 있다. ModelSerializer에서 사용된 유효성 검사 동작은 간단히 Serializer 클래스에서 복제할 수 있다.

         - 시리얼라이저 인스턴스의 repr을 출력하면 해당 인스턴스에 적용되는 유효성 검사 규칙이 명확히 드러난다. 모델 인스턴스에 숨겨진 추가적인 유효성 검사 동작이 호출되지 않는다.

         - ModelSerializer를 사용하면 이러한 모든 과정이 자동으로 처리된다. 사용자 정의 유효성 검사 동작을 추가하고 싶을 경우에도 간단하게 구현할 수 있다.

 

      - Example

         - Django REST 프레임워크에서 명시적인 유효성 검사를 사용하는 예시로, 고유성 제약 조건이 있는 필드를 가진 간단한 모델 클래스를 살펴보겠다.

class CustomerReportRecord(models.Model):
    time_raised = models.DateTimeField(default=timezone.now, editable=False)
    reference = models.CharField(unique=True, max_length=20)
    description = models.TextField()

 

         - CustomerReportRecord의 인스턴스를 생성하거나 업데이트하는 데 사용할 수 있는 기본 ModelSerializer이다.

class CustomerReportSerializer(serializers.ModelSerializer):
    class Meta:
        model = CustomerReportRecord

 

         - manage.py shell을 사용하여 Django 쉘을 열면 다음을 수행할 수 있다.

>>> from project.example.serializers import CustomerReportSerializer
>>> serializer = CustomerReportSerializer()
>>> print(repr(serializer))
CustomerReportSerializer():
    id = IntegerField(label='ID', read_only=True)
    time_raised = DateTimeField(read_only=True)
    reference = CharField(max_length=20, validators=[<UniqueValidator(queryset=CustomerReportRecord.objects.all())>])
    description = CharField(style={'type': 'textarea'})

 

         - 여기서 흥미로운 부분은 참조 필드(reference field)이다. 직렬화기 필드에 대해 명시적으로 유일성 제약 조건이 유효성 검사기에 의해 강제되고 있는 것을 볼 수 있다.

         - 이러한 명시적인 스타일 덕분에 REST framework에는 핵심 Django에는 없는 몇 가지 유효성 검사기 클래스가 포함되어 있다. 이러한 클래스에 대한 자세한 내용은 아래에서 설명한다. REST framework의 유효성 검사기는 Django의 유효성 검사기와 마찬가지로 __eq__ 메서드를 구현하여 인스턴스를 비교할 수 있도록 한다.

 

   3) UniqueValidator

      - 이 유효성 검사기는 모델 필드에 대한 unique=True 제약 조건을 강제하는 데 사용할 수 있다. 필수 인수 하나와 선택적인 메시지 인수를 받는다.

         - queryset (필수) - 고유성을 강제해야 하는 대상 쿼리셋이다.

         - message - 유효성 검사 실패 시 사용되는 오류 메시지이다.

         - lookup - 유효성 검사 중인 값과 일치하는 기존 인스턴스를 찾는 데 사용되는 조회이다. 기본값은 'exact'이다.

 

      - 이 유효성 검사기는 다음과 같이 직렬화기 필드에 적용해야 한다.

from rest_framework.validators import UniqueValidator

slug = SlugField(
    max_length=100,
    validators=[UniqueValidator(queryset=BlogPost.objects.all())]
)
from rest_framework import serializers

class MySerializer(serializers.Serializer):
    my_field = serializers.CharField(validators=[MyValidator(queryset=MyModel.objects.all())])

 

   4) UniqueTogetherValidator

      - 이 유효성 검사기는 모델 인스턴스에 대한 unique_together 제약 조건을 강제하는 데 사용할 수 있다. 필수 인수 두 개와 선택적인 메시지 인수 하나를 받는다.

         - queryset (필수) - 고유성을 강제해야 하는 대상 쿼리셋이다.

         - fields (필수) - 고유한 집합을 구성해야 하는 필드 이름의 리스트나 튜플입니다. 이들은 직렬화기 클래스의 필드로 존재해야 한다.

         - message - 유효성 검사 실패 시 사용되는 오류 메시지이다.

 

      - 이 유효성 검사기는 다음과 같이 직렬화기 클래스에 적용되어야 한다.

from rest_framework.validators import UniqueTogetherValidator

class ExampleSerializer(serializers.Serializer):
    # ...
    class Meta:
        # ToDo items belong to a parent list, and have an ordering defined
        # by the 'position' field. No two items in a given list may share
        # the same position.
        validators = [
            UniqueTogetherValidator(
                queryset=ToDoItem.objects.all(),
                fields=['list', 'position']
            )
        ]
from rest_framework import serializers, validators

class MySerializer(serializers.Serializer):
    field1 = serializers.CharField()
    field2 = serializers.CharField()

    validators = [
        validators.UniqueTogetherValidator(
            queryset=MyModel.objects.all(),
            fields=('field1', 'field2'),
            message='field1과 field2는 함께 고유해야 합니다.'
        )
    ]

 

      - 참고 : UniqueTogetherValidator 클래스는 항상 적용되는 모든 필드를 필수로 처리하는 암묵적인 제약 조건을 가지고 있다.

      - 그러나 기본값을 가진 필드는 이 제약 조건에서 예외이다.

      - 기본값이 있는 필드는 사용자 입력에서 누락되어도 기본값이 항상 제공되기 때문에 필수로 처리되지 않는다.

      - 이는 필드가 null=True 또는 blank=True로 설정되어 있지 않은 한 UniqueTogetherValidator가 누락된 필드에 대해 유효성 검사를 적용한다는 의미이다.


 

   5) UniqueForDateValidator

   6) UniqueForMonthValidator

   7) UniqueForYearValidator

      - 이러한 유효성 검사기(Validators)는 모델 인스턴스에 대해 unique_for_date, unique_for_month 및 unique_for_year 제약 조건을 강제할 수 있다. 다음과 같은 인수를 사용한다.
         - queryset (필수) - 유일성을 강제할 쿼리셋이다.
         - field (필수) - 지정된 날짜 범위에서 고유성을 검증할 필드 이름이다. 이는 직렬화 클래스의 필드로 존재해야 한다.
         - date_field (필수) - 고유성 제약의 날짜 범위를 결정하는 데 사용할 필드 이름이다. 이는 직렬화 클래스의 필드로 존재해야 한다.
         - message - 유효성 검사가 실패했을 때 사용할 오류 메시지이다.

 

      - 유효성 검사기(Validator)는 다음과 같이 serializer 클래스에 적용되어야 한다.

from rest_framework.validators import UniqueForYearValidator

class ExampleSerializer(serializers.Serializer):
    # ...
    class Meta:
        # Blog posts should have a slug that is unique for the current year.
        validators = [
            UniqueForYearValidator(
                queryset=BlogPostItem.objects.all(),
                field='slug',
                date_field='published'
            )
        ]

 

      - 유효성 검사에 사용되는 날짜 필드는 직렬화 클래스에 항상 존재해야 한다. 모델 클래스의 default=...에 의존하는 것만으로는 안된다. 왜냐하면 기본값이 생성되는 시점은 검사가 실행된 후이기 때문이다.

 

      - API의 동작 방식에 따라 사용할 수 있는 몇 가지 스타일이 있다. ModelSerializer를 사용하는 경우 REST framework가 자동으로 생성하는 기본값에 의존할 수 있지만, Serializer를 사용하거나 더 명시적인 제어가 필요한 경우에는 아래에 나열된 스타일 중 하나를 사용하면 된다.

 

      1. Using with a writable date field

         - 만약 날짜 필드를 쓰기 가능하게 하려면, 주의해야 할 점으로 기본 인수를 설정하거나 required=True로 설정하여 항상 입력 데이터에 사용 가능하도록 해야 한다는 것이다.

published = serializers.DateTimeField(required=True)

 

      2. Using with a read-only date field

         - 만약 날짜 필드를 사용자에게 보이게 하지만 편집은 불가능하게 하려면, read_only=True로 설정하고 추가로 default=... 인수를 설정해야 한다.

published = serializers.DateTimeField(read_only=True, default=timezone.now)

 

       3. Using with a hidden date field

         - 만약 날짜 필드를 사용자로부터 완전히 숨기고자 한다면, HiddenField를 사용해야 한다. 이 필드 유형은 사용자 입력을 받지 않고, 대신 직렬화기의 validated_data에 항상 기본값을 반환한다.

published = serializers.HiddenField(default=timezone.now)

      - 참고: UniqueFor<Range>Validator 클래스는 적용된 필드가 항상 필수로 처리되는 암묵적인 제약 조건을 부과한다. 하지만 기본값이 있는 필드는 이에 예외이며, 사용자 입력에서 누락되었을 때에도 항상 값을 제공한다.

from django.db import models
from django.core.validators import UniqueForDateValidator

class MyModel(models.Model):
    date_field = models.DateField()
    other_field = models.CharField(max_length=100)

    class Meta:
        constraints = [
            UniqueForDateValidator(
                date_field='date_field',
                message='같은 날짜에는 다른 필드 값이 고유해야 합니다.'
            )
        ]

    def __str__(self):
        return self.other_field

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)


      - 참고: HiddenField()은 partial=True 시리얼라이저(partial=True로 PATCH 요청을 하는 경우)에 나타나지 않는다. 이 동작은 나중에 변경될 수 있으므로 GitHub 토론을 업데이트하는걸 권고한다.


 

 2. 고급 필드 기본값

   - 시리얼라이저에서 여러 필드에 적용되는 검증기는 때로는 API 클라이언트가 제공해서는 안 되는 필드 입력을 요구할 수 있다. 그러나 해당 필드는 검증기에 입력으로 사용할 수 있다.

   - 이러한 유형의 검증에 사용할 수 있는 두 가지 패턴은 다음과 같다.

      - HiddenField를 사용하는 방법이다. 이 필드는 validated_data에는 존재하지만, serializer 출력 표현에는 사용되지 않는다.

      - 사용자가 직접 설정할 수 없지만 직렬화된 출력 표현에 사용되는 read_only=True로 설정된 표준 필드를 사용하는 방법이다. 이 필드에는 default=... 인자도 포함된다.

 

   - REST 프레임워크에는 이와 관련하여 유용한 몇 가지 기본값이 포함되어 있을 수 있다.

   1) CurrentUserDefault

      - 현재 사용자를 나타내는 데 사용할 수 있는 기본 클래스가 있다. 이를 사용하기 위해서는 시리얼라이저를 인스턴스화할 때 'request'가 context 사전의 일부로 제공되어야 한다.

owner = serializers.HiddenField(
    default=serializers.CurrentUserDefault()
)
from rest_framework import serializers
from rest_framework.request import Request
from rest_framework.serializers import CurrentUserDefault

class MySerializer(serializers.Serializer):
    user = serializers.HiddenField(default=CurrentUserDefault())

    def create(self, validated_data):
        # 생성 작업 수행
        user = validated_data['user']
        # ...
        return instance

 

   2) CreateOnlyDefault

      - 생성 작업 중에만 기본 인자를 설정하는 데 사용할 수 있는 기본 클래스가 있다. 업데이트 중에는 해당 필드가 생략된다.
      - 이 클래스는 단일 인자를 받으며, 이는 생성 작업 중에 사용될 기본값 또는 호출 가능한 객체이다.

created_at = serializers.DateTimeField(
    default=serializers.CreateOnlyDefault(timezone.now)
)
from rest_framework import serializers
from rest_framework.serializers import CreateOnlyDefault

class MySerializer(serializers.Serializer):
    my_field = serializers.CharField(default=CreateOnlyDefault('default_value'))

    def create(self, validated_data):
        # 생성 작업 수행
        my_field_value = validated_data['my_field']
        # ...
        return instance

 

 3. Validators의 제한 사항

   - 모델 시리얼라이저가 생성하는 기본 시리얼라이저 클래스에 의존하는 대신 명시적으로 유효성 검사를 처리해야 하는 애매한 경우가 있다.

   - 이러한 경우에는 시리얼라이저 Meta.validators 속성에 빈 리스트를 지정하여 자동으로 생성된 유효성 검사기를 비활성화할 수 있다.

 

   1) 선택적 필드

      - 기본적으로 "unique together" 유효성 검사는 모든 필드에 required=True를 적용한다. 그러나 경우에 따라서는 하나의 필드에 required=False를 명시적으로 적용하고자 할 수 있다. 이 경우 유효성 검사의 원하는 동작이 애매해질 수 있다.

 

      - 이 경우에는 일반적으로 시리얼라이저 클래스에서 유효성 검사기를 제외하고, 대신 .validate() 메서드나 뷰에서 명시적으로 유효성 검사 로직을 작성해야 한다.

class BillingRecordSerializer(serializers.ModelSerializer):
    def validate(self, attrs):
        # Apply custom validation either here, or in the view.

    class Meta:
        fields = ['client', 'date', 'amount']
        extra_kwargs = {'client': {'required': False}}
        validators = []  # Remove a default "unique together" constraint.
class BillingRecordSerializer(serializers.ModelSerializer):
    def validate(self, attrs):
        # Custom validation logic
        if attrs.get('client') is None:
            raise serializers.ValidationError("client 필드는 필수입니다.")
        
        # 추가적인 유효성 검사 로직을 구현할 수 있습니다.

        return attrs

    class Meta:
        fields = ['client', 'date', 'amount']
        extra_kwargs = {'client': {'required': False}}
        validators = []  # Remove a default "unique together" constraint.

 

   2) 중첩된 시리얼라이저 업데이트

      - 기존 인스턴스에 업데이트를 적용할 때, 고유성 검사기(uniqueness validators)는 현재 인스턴스를 고유성 검사에서 제외한다. 이는 serializer의 속성으로 현재 인스턴스가 존재하기 때문입니다. serializer를 인스턴스화할 때, 초기에는 instance=...와 같은 방식으로 현재 인스턴스가 전달되었습니다. 따라서, 고유성 확인의 컨텍스트에서는 현재 인스턴스를 사용할 수 있습니다.

 

      - nested serializers에서 업데이트 작업을 수행하는 경우에는 이 제외를 적용할 수 있는 방법이 없다. 왜냐하면 인스턴스가 사용 불가능하기 때문이다.

 

      - 다시 말하자면, 아마도 직렬화기 클래스에서 검증기(validator)를 명시적으로 제거하고, 검증 제약 조건을 .validate() 메서드나 뷰(view)에서 명시적으로 작성해야 할 것이다.

# serializers.py
from rest_framework import serializers

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = Child
        fields = ('child_name',)

class ParentSerializer(serializers.ModelSerializer):
    children = ChildSerializer(many=True, read_only=True)

    class Meta:
        model = Parent
        fields = ('id', 'name', 'children')

    def validate_name(self, value):
        # Custom validation logic for the name field
        instance = getattr(self, 'instance', None)
        if instance and instance.name == value:
            return value  # Allow the same name during update

        # Check if the name is unique among existing Parent instances
        existing_parent = Parent.objects.filter(name=value).exclude(id=instance.id if instance else None).first()
        if existing_parent:
            raise serializers.ValidationError("Name must be unique among parents.")
        
        return value

 


from rest_framework import serializers
from .models import User, Profile

class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ['address', 'phone']

    def validate_address(self, value):
        if self.instance:
            if Profile.objects.exclude(pk=self.instance.pk).filter(address=value).exists():
                raise serializers.ValidationError("This address is already in use.")
        else:
            if Profile.objects.filter(address=value).exists():
                raise serializers.ValidationError("This address is already in use.")
        return value

    def validate_phone(self, value):
        if self.instance:
            if Profile.objects.exclude(pk=self.instance.pk).filter(phone=value).exists():
                raise serializers.ValidationError("This phone number is already in use.")
        else:
            if Profile.objects.filter(phone=value).exists():
                raise serializers.ValidationError("This phone number is already in use.")
        return value


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

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

    def update(self, instance, validated_data):
        profile_data = validated_data.pop('profile')
        profile = instance.profile

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

        # Pass the instance to the ProfileSerializer
        profile_serializer = ProfileSerializer(instance=profile, data=profile_data)
        if profile_serializer.is_valid(raise_exception=True):
            profile_serializer.save()

        return instance

 

      - 위 코드에서 validate_address와 validate_phone 메서드는 각각 address와 phone 값이 이미 사용 중인지 확인한다. 이때 이미 존재하는 인스턴스를 업데이트하는 경우에는 self.instance가 있으므로, Profile.objects.exclude(pk=self.instance.pk).filter(...)를 사용하여 현재 인스턴스를 제외하고 체크를 한다.

      - 또한, UserSerializer의 update 메서드에서는 ProfileSerializer를 직접 인스턴스화하고 instance=profile를 전달하여 기존 인스턴스를 업데이트한다. 이렇게 하면 ProfileSerializer의 validate_... 메서드에서 self.instance를 사용할 수 있다. 이렇게 하면 nested serializer에서도 기존 인스턴스를 업데이트할 때 유니크 체크를 정확히 할 수 있다.


 

   3) 복잡한 경우의 디버깅

      - 만약 ModelSerializer 클래스가 어떤 동작을 생성하는지 정확히 확신이 없다면, 일반적으로는 manage.py shell을 실행하여 직렬화기의 인스턴스를 출력하는 것이 좋은 방법이다. 이렇게 하면 자동으로 생성되는 필드와 검증기를 검토할 수 있다.

>>> serializer = MyComplexModelSerializer()
>>> print(serializer)
class MyComplexModelSerializer:
    my_fields = ...

 

      - 또한 복잡한 경우에는 기본 ModelSerializer 동작에 의존하는 대신 serializer 클래스를 명시적으로 정의하는 것이 더 나을 수 있다. 이렇게 하면 좀 더 코드를 작성해야하지만, 결과적으로 동작이 더 투명해진다.

 

 4. 사용자 정의 유효성 검사기 작성

   - Django에서는 기존의 검증기(validators)를 사용하거나 사용자 정의 검증기를 작성할 수 있다.

   1) 함수 기반

      - 검증기(validator)는 실패 시 serializers.ValidationError을 발생시킬 수 있는 어떤 호출 가능한(callable) 객체든 될 수 있다.

def even_number(value):
    if value % 2 != 0:
        raise serializers.ValidationError('This field must be an even number.')
from rest_framework import serializers
from .models import User

def even_number(value):
    if value % 2 != 0:
        raise serializers.ValidationError('This field must be an even number.')

class UserSerializer(serializers.ModelSerializer):
    age = serializers.IntegerField(validators=[even_number])

    class Meta:
        model = User
        fields = ['email', 'name', 'age']

 

      1. Field-level validation

         - Serializer 하위 클래스에 .validate_<field_name> 메서드를 추가하여 사용자 정의 필드 수준의 검증을 지정할 수 있다. 이에 대한 자세한 내용은 Serializer 문서에서 확인할 수 있다.

 

https://www.django-rest-framework.org/api-guide/serializers/#field-level-validation

 

Serializers - Django REST framework

 

www.django-rest-framework.org

 

   2) 클래스 기반

      - 클래스 기반의 검증기를 작성하려면 call 메서드를 사용하시면 된다. 클래스 기반의 검증기는 동작을 매개변수화하고 재사용할 수 있으므로 유용하다.

class MultipleOf:
    def __init__(self, base):
        self.base = base

    def __call__(self, value):
        if value % self.base != 0:
            message = 'This field must be a multiple of %d.' % self.base
            raise serializers.ValidationError(message)
from rest_framework import serializers
from .models import User

class MultipleOf:
    def __init__(self, base):
        self.base = base

    def __call__(self, value):
        if value % self.base != 0:
            message = 'This field must be a multiple of %d.' % self.base
            raise serializers.ValidationError(message)

class UserSerializer(serializers.ModelSerializer):
    age = serializers.IntegerField(validators=[MultipleOf(5)])

    class Meta:
        model = User
        fields = ['email', 'name', 'age']

      1. Accessing the context

         - 고급 경우에는 검증기가 사용되는 직렬화기 필드를 추가적인 컨텍스트로 전달받기를 원할 수 있다. 이를 위해 검증기 클래스에 requires_context = True 속성을 설정할 수 있다. 그러면 call 메서드가 serializer_field 또는 serializer와 함께 추가 인자로 호출된다.

class MultipleOf:
    requires_context = True

    def __call__(self, value, serializer_field):
        ...
from rest_framework import serializers
from .models import User

class MultipleOf:
    requires_context = True

    def __init__(self, base):
        self.base = base

    def __call__(self, value, serializer_field):
        if value % self.base != 0:
            message = 'This field must be a multiple of %d.' % self.base
            raise serializers.ValidationError(message)
        print(serializer_field.context)  # 접근 가능한 context를 출력

class UserSerializer(serializers.ModelSerializer):
    age = serializers.IntegerField(validators=[MultipleOf(5)])

    class Meta:
        model = User
        fields = ['email', 'name', 'age']

 

         - 위의 코드에서 MultipleOf 클래스의 __call__ 메서드가 이제 두 개의 매개변수 value, serializer_field를 받는다. 이는 requires_context가 True로 설정되었기 때문이다.

         - serializer_field 객체를 통해 context 속성에 접근할 수 있다. 이 context는 Serializers의 context와 동일하며, 보통 request, view, format 등의 정보를 포함한다. 이를 통해 HTTP 요청 정보에 접근하거나, 현재 처리 중인 view의 정보를 참조하는 등의 작업을 수행할 수 있다.

         - 예를 들어, serializer_field.context['request'].user을 통해 현재 요청을 보낸 사용자의 정보를 얻을 수 있다.

 

 - 공식 사이트 문서 : https://www.django-rest-framework.org/api-guide/validators/#validators

 

Validators - Django REST framework

 

www.django-rest-framework.org

 

댓글