Serialization in DRF is a two-phase process that bridges the gap between complex Python objects and simple data types:
# From rest_framework/serializers.py:6-11"""Serialization in REST framework is a two-phase process:1. Serializers marshal between complex types like model instances, and python primitives.2. The process of marshaling between python primitives and request and response content is handled by parsers and renderers."""
The foundation of all serializers. It enforces strict usage patterns:
# From rest_framework/serializers.py:89-112class BaseSerializer(Field): """ If a `data=` argument is passed then: .is_valid() - Available. .initial_data - Available. .validated_data - Only available after calling `is_valid()` .errors - Only available after calling `is_valid()` .data - Only available after calling `is_valid()` If a `data=` argument is not passed then: .is_valid() - Not available. .initial_data - Not available. .validated_data - Not available. .errors - Not available. .data - Available. """
The serializer’s interface changes based on usage mode:
# When you pass data= you're deserializing (validating input)serializer = ArticleSerializer(data=request.data)if serializer.is_valid(): article = serializer.save() # Create from validated data return Response(serializer.data)return Response(serializer.errors, status=400)
Accessing .data before calling .is_valid() when you passed data= raises an AssertionError. This prevents you from accidentally returning unvalidated data.
This method converts Python objects to native datatypes:
# From rest_framework/serializers.py:530-554def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. """ ret = {} fields = self._readable_fields for field in fields: try: attribute = field.get_attribute(instance) except SkipField: continue # Skip `to_representation` for `None` values if attribute is None: ret[field.field_name] = None else: ret[field.field_name] = field.to_representation(attribute) return ret
class ArticleSerializer(serializers.ModelSerializer): def validate_title(self, value): """ Called for the 'title' field. Method name: validate_<field_name> """ if len(value) < 5: raise serializers.ValidationError( "Title must be at least 5 characters" ) return value
class ArticleSerializer(serializers.ModelSerializer): def validate(self, attrs): """ Called with all fields after field-level validation. Useful for validation that requires multiple fields. """ if attrs['publish_date'] < attrs['created']: raise serializers.ValidationError( "Publish date cannot be before creation date" ) return attrs
# From rest_framework/serializers.py:446-464def run_validation(self, data=empty): """ We override the default `run_validation`, because the validation performed by validators and the `.validate()` method should be coerced into an error dictionary. """ (is_empty_value, data) = self.validate_empty_values(data) if is_empty_value: return data value = self.to_internal_value(data) # 1. Field-level validation try: self.run_validators(value) # 2. Validator classes value = self.validate(value) # 3. Object-level validation assert value is not None, '.validate() should return the validated data' except (ValidationError, DjangoValidationError) as exc: raise ValidationError(detail=as_serializer_error(exc)) return value
Validation happens in is_valid(), which calls run_validation() internally.
The .save() method orchestrates create/update operations:
# From rest_framework/serializers.py:177-215def save(self, **kwargs): assert hasattr(self, '_errors'), ( 'You must call `.is_valid()` before calling `.save()`.' ) assert not self.errors, ( 'You cannot call `.save()` on a serializer with invalid data.' ) validated_data = {**self.validated_data, **kwargs} if self.instance is not None: self.instance = self.update(self.instance, validated_data) assert self.instance is not None, ( '`update()` did not return an object instance.' ) else: self.instance = self.create(validated_data) assert self.instance is not None, ( '`create()` did not return an object instance.' ) return self.instance
class ArticleSerializer(serializers.ModelSerializer): def create(self, validated_data): """ Called by .save() when no instance was provided. """ return Article.objects.create(**validated_data)# Usageserializer = ArticleSerializer(data=request.data)if serializer.is_valid(): article = serializer.save() # Calls create()
# Pass extra data that isn't in the requestserializer.save(author=request.user, ip_address=request.META['REMOTE_ADDR'])# This data is merged with validated_data:validated_data = {**self.validated_data, **kwargs}
ModelSerializer automatically generates fields from Django models:
# From rest_framework/serializers.py:910-925class ModelSerializer(Serializer): """ A `ModelSerializer` is just a regular `Serializer`, except that: * A set of default fields are automatically populated. * A set of default validators are automatically populated. * Default `.create()` and `.update()` implementations are provided. """
# From rest_framework/serializers.py:1068-1147 (simplified)def get_fields(self): """ Return the dict of field names -> field instances. """ declared_fields = copy.deepcopy(self._declared_fields) model = getattr(self.Meta, 'model') # Get metadata about the model info = model_meta.get_field_info(model) field_names = self.get_field_names(declared_fields, info) fields = {} for field_name in field_names: # Use explicitly declared fields if field_name in declared_fields: fields[field_name] = declared_fields[field_name] continue # Otherwise, build field from model field_class, field_kwargs = self.build_field( field_name, info, model, depth ) fields[field_name] = field_class(**field_kwargs) return fields
By default, nested serializers are read-only. For writable nested data, you must override create() or update():
# From rest_framework/serializers.py:828-851def raise_errors_on_nested_writes(method_name, serializer, validated_data): """ Writable nested relationships and dotted-source fields are intentionally unsupported by default due to ambiguous persistence semantics. Developers must either: - Override the `.create()` / `.update()` methods explicitly, or - Mark nested serializers as `read_only=True` """
Attempting to save nested data without overriding create()/update() will raise an assertion error. This is intentional - DRF doesn’t know whether to create, update, or ignore nested objects.
# From rest_framework/serializers.py:719-729def to_representation(self, data): """ List of object instances -> List of dicts of primitive datatypes. """ iterable = data.all() if isinstance(data, models.manager.BaseManager) else data return [ self.child.to_representation(item) for item in iterable ]
# From rest_framework/serializers.py:314-349def as_serializer_error(exc): """ Coerce validation exceptions into a standardized serialized error format. The returned structure conforms to: - Field-specific errors: '{field-name: [errors]}' - Non-field errors: under the 'NON_FIELD_ERRORS_KEY' """
class ArticleSerializer(serializers.ModelSerializer): class Meta: model = Article fields = ['id', 'title', 'author'] depth = 1 # Auto-expand one level of relations
Using depth is convenient but can lead to over-fetching. For production APIs, explicitly define nested serializers for better control.
# Good: One serializer per purposeclass ArticleListSerializer(serializers.ModelSerializer): """Light serializer for list views""" class Meta: model = Article fields = ['id', 'title', 'created']class ArticleDetailSerializer(serializers.ModelSerializer): """Full serializer for detail views""" class Meta: model = Article fields = ['id', 'title', 'content', 'author', 'created', 'updated']
# Business logic validation: in serializerclass ArticleSerializer(serializers.ModelSerializer): def validate_title(self, value): if 'spam' in value.lower(): raise serializers.ValidationError("Title contains spam words") return value# Data integrity validation: in modelclass Article(models.Model): title = models.CharField(max_length=200) def clean(self): if len(self.title) < 5: raise ValidationError("Title too short")