[Django] DRF Serializer 심화
정의
DRF Serializer는 JSON ↔ Python ↔ Model의 변환과 검증을 담당한다. Django Form과 비슷한 인터페이스이나 API/JSON에 특화. 검증, 직렬화, nested 관계, 동적 필드, custom validation 등 풍부.
두 방향으로 동작한다
Serializer 는 항상 두 방향 중 하나 로 호출된다. 이 구분이 헷갈리면 코드도 헷갈린다.
| 방향 | 호출 형태 | 얻는 것 | 용도 |
|---|---|---|---|
| 직렬화 (Serialize) | Serializer(instance) | .data (dict) | 응답: Model → JSON |
| 역직렬화 (Deserialize) | Serializer(data=request.data) | .validated_data after .is_valid(), .save() | 요청: JSON → Model |
# 방향 ①: 직렬화 (응답용)
post = Post.objects.get(pk=1)
serializer = PostSerializer(post) # instance= 인자
serializer.data # {"id": 1, "title": "..."}
# 방향 ②: 역직렬화 (요청용)
serializer = PostSerializer(data=request.data) # data= 인자 (키워드 필수!)
if serializer.is_valid():
post = serializer.save() # validated_data → DB
else:
print(serializer.errors)
IMPORTANT
data= 는 반드시 키워드 인자. PostSerializer(request.data) 는 첫 번째 위치 인자 = instance 로 해석되어 잘못된 결과.
Serializer vs ModelSerializer
# 일반 Serializer
class CommentSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
post_id = serializers.IntegerField()
body = serializers.CharField(max_length=500)
created_at = serializers.DateTimeField(read_only=True)
def create(self, validated_data):
return Comment.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.body = validated_data.get("body", instance.body)
instance.save()
return instance
# ModelSerializer (자동)
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "post_id", "body", "created_at"]
read_only_fields = ["id", "created_at"]
ModelSerializer는 모델에서 필드 자동 추론 + create/update 자동.
필드
class UserSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
email = serializers.EmailField()
name = serializers.CharField(max_length=100, allow_blank=True)
age = serializers.IntegerField(min_value=0, max_value=150, required=False)
bio = serializers.CharField(required=False, allow_null=True)
role = serializers.ChoiceField(choices=["admin", "user"])
avatar = serializers.URLField()
birthday = serializers.DateField(format="%Y-%m-%d")
created_at = serializers.DateTimeField(read_only=True)
tags = serializers.ListField(child=serializers.CharField())
meta = serializers.JSONField(default=dict)
secret = serializers.CharField(write_only=True) # 응답에 노출 X
옵션:
required(기본 True)defaultallow_nullallow_blank(string)read_only,write_onlysource(모델 필드명과 다를 때)style(HTML 위젯 힌트)validators
관계 필드
PrimaryKeyRelatedField
class PostSerializer(serializers.ModelSerializer):
author = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())
tags = serializers.PrimaryKeyRelatedField(queryset=Tag.objects.all(), many=True)
class Meta:
model = Post
fields = ["id", "title", "author", "tags"]
요청·응답에 ID만:
{"id": 1, "title": "...", "author": 5, "tags": [1, 2]}
StringRelatedField (read_only)
author = serializers.StringRelatedField()
# author의 __str__() 값
Nested serializer
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ["id", "name"]
class PostSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
author = UserSerializer(read_only=True)
author_id = serializers.PrimaryKeyRelatedField(
source="author", queryset=User.objects.all(), write_only=True
)
class Meta:
model = Post
fields = ["id", "title", "author", "author_id", "tags"]
응답: nested 객체. 요청: author_id로 단순.
HyperlinkedRelatedField
author = serializers.HyperlinkedRelatedField(view_name="user-detail", read_only=True)
# {"author": "/api/users/5/"}
REST의 HATEOAS 추구 시.
SerializerMethodField
동적 계산 필드 (read-only).
class PostSerializer(serializers.ModelSerializer):
comment_count = serializers.SerializerMethodField()
is_liked = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ["id", "title", "comment_count", "is_liked"]
def get_comment_count(self, obj):
return obj.comments.count()
def get_is_liked(self, obj):
request = self.context.get("request")
if request and request.user.is_authenticated:
return obj.likes.filter(user=request.user).exists()
return False
get_<field_name> 메서드. context로 view에서 추가 데이터.
검증
빌트인
각 필드의 type/required/length/regex.
필드 레벨 (validate_<field>)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["email", "name"]
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("이미 가입된 이메일")
return value.lower()
객체 레벨 (validate)
여러 필드 관계.
def validate(self, data):
if data.get("password") != data.get("password_confirm"):
raise serializers.ValidationError({"password_confirm": "일치하지 않음"})
return data
Validators 인자
from django.core.validators import RegexValidator
phone = serializers.CharField(validators=[
RegexValidator(r"^\d{3}-\d{4}-\d{4}$"),
UniqueValidator(queryset=User.objects.all()), # DRF 내장
])
UniqueTogetherValidator
class Meta:
validators = [
UniqueTogetherValidator(queryset=Post.objects.all(), fields=["author", "slug"])
]
create / update 오버라이드
nested 관계의 저장은 직접 구현.
class PostSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True)
class Meta:
model = Post
fields = ["id", "title", "tags"]
def create(self, validated_data):
tags_data = validated_data.pop("tags")
post = Post.objects.create(**validated_data)
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(name=tag_data["name"])
post.tags.add(tag)
return post
def update(self, instance, validated_data):
tags_data = validated_data.pop("tags", None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
if tags_data is not None:
instance.tags.clear()
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(name=tag_data["name"])
instance.tags.add(tag)
return instance
context
view에서 serializer로 전달.
# ViewSet
def get_serializer_context(self):
context = super().get_serializer_context()
context["extra"] = "..."
return context
# 또는 직접
PostSerializer(post, context={"request": request})
# Serializer에서
def get_url(self, obj):
request = self.context["request"]
return request.build_absolute_uri(obj.get_absolute_url())
기본 context: request, view, format.
동적 필드
요청에 따라 다른 필드 노출.
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
fields = kwargs.pop("fields", None)
super().__init__(*args, **kwargs)
if fields:
allowed = set(fields)
existing = set(self.fields.keys())
for name in existing - allowed:
self.fields.pop(name)
# 사용
UserSerializer(user, fields=["id", "email"])
또는 ViewSet에서 query param으로 분기.
ListSerializer
many=True 시 ListSerializer가 사용됨. bulk create/update 오버라이드.
class BookListSerializer(serializers.ListSerializer):
def create(self, validated_data):
books = [Book(**item) for item in validated_data]
return Book.objects.bulk_create(books)
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = "__all__"
list_serializer_class = BookListSerializer
자주 보는 패턴
Read vs Write 분리
class PostReadSerializer(ModelSerializer):
author = UserSerializer()
tags = TagSerializer(many=True)
class Meta:
model = Post
fields = ["id", "title", "body", "author", "tags"]
class PostWriteSerializer(ModelSerializer):
tag_ids = PrimaryKeyRelatedField(source="tags", many=True, queryset=Tag.objects.all())
class Meta:
model = Post
fields = ["title", "body", "tag_ids"]
class PostViewSet(ModelViewSet):
def get_serializer_class(self):
if self.action in ["list", "retrieve"]:
return PostReadSerializer
return PostWriteSerializer
Field source 변환
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source="get_full_name", read_only=True)
avatar_url = serializers.SerializerMethodField()
def get_avatar_url(self, obj):
if obj.avatar:
return self.context["request"].build_absolute_uri(obj.avatar.url)
return None
Nested write with PK
class CommentSerializer(serializers.ModelSerializer):
post = serializers.PrimaryKeyRelatedField(queryset=Post.objects.all())
author_email = serializers.EmailField(source="author.email", read_only=True)
class Meta:
model = Comment
fields = ["id", "post", "body", "author_email"]
함정
1. ModelSerializer + fields=“all”
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = "__all__" # password 해시 노출!
명시 권장. password, internal 필드 제외.
2. nested validation
nested 객체 검증 실패 시 에러 메시지가 깊게 nested.
{"tags": [{"name": ["이미 존재"]}]}
UI 처리 주의.
3. partial update (PATCH)
serializer = PostSerializer(post, data=request.data, partial=True)
partial=True로 required 필드 무시.
4. context[“request”] 없으면 None
테스트나 직렬화 단독 호출 시 request 없음. defensive get.
request = self.context.get("request")
if request is None:
return None
5. N+1 (SerializerMethodField)
def get_comment_count(self, obj):
return obj.comments.count() # 매 instance마다 SQL
→ ViewSet에서 annotate(comment_count=Count("comments")) 후 필드:
comment_count = serializers.IntegerField(read_only=True)
💬 댓글