본문으로 건너뛰기
김신건의 로그

[Django] DRF Serializer 심화

· 수정 · 📖 약 1분 · 503자/단어 #django #drf #serializer #api
drf serializer, ModelSerializer, nested serializer, SerializerMethodField, drf validation

정의

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)
  • default
  • allow_null
  • allow_blank (string)
  • read_only, write_only
  • source (모델 필드명과 다를 때)
  • 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)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기