wafflestudio / seminar-2020

2020 Rookies 세미나
28 stars 49 forks source link

serializer에서 BooleanField의 값이 존재하는지 validation을 하지 않는 것 같습니다 #218

Open gina0605 opened 4 years ago

gina0605 commented 4 years ago

문제 상황

POST /api/v1/user/participant/ 를 구현하는 과정에서 발견한 현상입니다. accepted의 값을 넘겨주지 않았음에도 is_valid() 결과가 True였습니다. 이 현상을 테스트하기 위해 새로운 model과 serializer를 만들어보았습니다.

테스트

새로운 app booleanfield를 만들어 테스트했습니다.

booleanfield/models.py

from django.db import models

class TestModel(models.Model):
    bf = models.BooleanField()

booleanfield/serializers.py

from rest_framework import serializers
from booleanfield.models import TestModel

class TestSerializer(serializers.ModelSerializer):
    bf = serializers.BooleanField(required=True)
    class Meta:
        model = TestModel
        fields = ('bf',)

python manage.py shell

>>> from booleanfield.serializers import TestSerializer
>>> from django.http import QueryDict
>>> 
>>> d = {}
>>> TestSerializer(data=d).is_valid()
False
>>> qd = QueryDict()
>>> print(qd)
<QueryDict: {}>
>>> TestSerializer(data=qd).is_valid()
True

컴퓨터 환경

settings.py는 제공받은 waffle_backend의 settings.py에서 INSTALLED_APPSTIME_ZONE만 수정했습니다. 아래는 제 파이썬 환경입니다.

Package              Version
-------------------- -------
asgiref              3.2.10
Django               3.1
django-debug-toolbar 2.2
djangorestframework  3.11.1
mysqlclient          2.0.1
pip                  20.2.2
Pygments             2.7.1
pytz                 2020.1
setuptools           50.0.0
sqlparse             0.3.1
wheel                0.35.1

질문

gina0605 commented 4 years ago

참고로 BooleanField 대신 NullBooleanField를 이용하자 is_valid() 값은 항상 False로, 제가 생각하기에 합당한 결과가 나왔습니다.

YeonghyeonKO commented 4 years ago

혹시 request에 필요한 값을 날리시기 전에 정의한 serializer나 model에서 field가 required=False 또는 blank=True 설정은 없는 상황인가요?

gina0605 commented 4 years ago

@YeonghyeonKO 네, serializer에서는 required=True만 설정해두었고(사실 설정하지 않아도 default가 required=True인 것 같긴 합니다) model에서는 딱히 설정한 것이 없습니다.

gina0605 commented 4 years ago

자답합니다.

우선, http request를 보낼 때 body에 보내는 내용은 DRF의 Parser에서 처리하게 됩니다. 이를 통해서 request.dataQueryDict object가 들어있게 되는 것입니다. 다만, http request의 body가 없을 경우 QueryDict가 아니라 Python dictionary가 되는 것 같습니다.

BooleanField의 값이 없어도 is_valid를 통과하는 이유는 DRF BooleanField document를 보면 알 수 있습니다. HTML incode input의 경우 BooleanField의 값이 주어지지 않아도 False로 처리된다고 적혀있습니다.

When using HTML encoded form input be aware that omitting a value will always be treated as setting a field to False, even if it has a default=True option specified. This is because HTML checkbox inputs represent the unchecked state by omitting the value, so REST framework treats omission as if it is an empty checkbox input.

코드를 살펴보면, serializer의 각 field 값을 주어진 data에서 읽는 메소드인 get_value()가 있습니다. BooleanField의 경우 default_empty_htmlFalse로 설정되어 있습니다. 그렇기 때문에 값을 입력하지 않아도 저절로 False로 처리되는 것입니다. NullBooleanField의 경우에는 default_empty_htmlempty로 설정되어 있습니다. empty는 DRF에서 정의한 class로, 입력된 값이 없는 경우를 의미합니다. (None 자체가 유의미한 값일 수 있기 때문에, 이와 구분하기 위해 존재합니다.) required=True이고 get_value()에서 값이 empty일 경우 is_valid() 값이 False가 됩니다.

TLDR

gina0605 commented 4 years ago

도큐멘트에 적혀있는 내용인데 한참 헤맸네요ㅜㅜ 과제는 NullBooleanField를 이용해 해결했습니다.

YeonghyeonKO commented 4 years ago

아 sql에서 db 보셨으면 모든 accepted가 false로 되는 이유를 설명해주신거 같습니다. 저도 비슷한 상황이 왔었는데 처리만 했었지 이유까지는 알아보지 않았는데 감사합니다!

davin111 commented 4 years ago

과제 2 당시 이 부분의 명세가 불명료한 지점이 있었던 것 같은데, 일단 의도는 POST /api/v1/user/POST /api/v1/user/participant/ request의 body에서 accepted는 optional하고, 없는 경우 True라는 것이었습니다. accepted는 optional하고 default로 한 쪽의 값을 갖는 것이 일반적인 서비스의 동작을 고려했을 때 좀 더 자연스러운 구현이라고 생각되고, 반대로 accepted가 없을 때 400이어야 한다는 명세도 없었긴 했습니다.

어쨌든 과제 2의 이 부분에 대해서는 어느 쪽으로 구현하셨어도 자연스러운 것으로 받아들이도록 하겠습니다. 다만 과제 3을 진행할 때는, #221 과 관련해 optional한 것으로 취급하고, default 값을 True인 것으로 수정해주세요.

davin111 commented 4 years ago

@gina0605 @YeonghyeonKO 관련 내용 과제 2, 과제 3에도 추가했습니다! 감사합니다. https://github.com/wafflestudio/rookies/commit/f2d66399890043cdfbfaedc5a5c2513a958761ae