In Django REST Framework, what do to_representation() and to_internal_value() do, and when is overriding them the right tool?
answer
- two directions of conversion
- .data versus is_valid()
- call super() and adjust
- a custom Field for reuse
basics
~20 sto_representation() turns an object into primitive output for .data; to_internal_value() turns request data into the validated dict during is_valid(). Override them to reshape a representation, usually by calling super() and adjusting, or write a custom Field for a reusable conversion.
solid answer
~40 sEvery DRF serializer and field converts in two directions. `to_representation(instance)` builds the output: the serializer walks its readable fields, fetches each value through the field's `source`, puts `None` straight into the output, and otherwise calls that field's `to_representation()`; `.data` calls it. `to_internal_value(data)` goes the other way during `is_valid()`: the serializer checks the input is a mapping, runs each writable field's validation, collects errors per field and raises one `ValidationError`, or returns the dict that becomes `validated_data`. I override the serializer's methods for whole-object reshaping, such as adding an `in_stock` flag computed from `stock` or accepting a legacy key name, calling `super()` and editing the result. For a conversion that recurs, such as a price sent as `{"amount", "currency"}`, I write a custom `serializers.Field` with both methods instead.
code
python · 34 linesfrom decimal import Decimal, InvalidOperation
from rest_framework import serializers
from catalog.models import Product
class EuroPriceField(serializers.Field):
# {"amount": "19.99", "currency": "EUR"} on the wire, Decimal inside
def to_representation(self, value):
return {"amount": f"{value:.2f}", "currency": "EUR"}
def to_internal_value(self, data):
if not isinstance(data, dict) or "amount" not in data:
raise serializers.ValidationError('Expected {"amount": ..., "currency": "EUR"}.')
if data.get("currency", "EUR") != "EUR":
raise serializers.ValidationError("Only EUR prices are accepted.")
try:
return Decimal(str(data["amount"]))
except InvalidOperation:
raise serializers.ValidationError("Amount must be a decimal number.")
class ProductSerializer(serializers.ModelSerializer):
price = EuroPriceField()
class Meta:
model = Product
fields = ["id", "sku", "name", "price", "stock"]
def to_representation(self, instance):
data = super().to_representation(instance)
data["in_stock"] = instance.stock > 0
return datago deeper
Know that to_representation() produces the output you read from .data and to_internal_value() converts request data during is_valid().
Explain the default loops: readable fields, source lookup and the None shortcut on the way out; mapping check, per-field validation and error collection on the way in.
Choose between a serializer override, a custom Field and a SerializerMethodField, keep round-trips symmetric, and avoid hiding contract changes from schema tools.
Decide where representation logic lives across an API so that versioned or legacy shapes stay explicit, documented and testable rather than scattered overrides.
## A serializer is a two-way converter In Django REST Framework, a serializer is itself a kind of field, and every field has the same pair of methods: | Direction | Method | Triggered by | Input | Output | |---|---|---|---|---| | object to wire | `to_representation(instance)` | reading `serializer.data` | a model instance or other object | a dict of primitives (or a primitive, for a field) | | wire to Python | `to_internal_value(data)` | `serializer.is_valid()` | parsed request data | a dict of native values (or a value, for a field) | A plain `Serializer` subclass inherits working implementations of both from `Serializer`; the abstract base raises `NotImplementedError` for them. Overriding means changing one step of a well-defined loop, so it helps to know the loop. ## What the default to_representation does 1. It iterates over the serializer's **readable** fields (every field that is not write-only). 2. For each, it fetches the attribute through the field's **`source`**, which defaults to the field name. 3. If the value is `None`, it writes `None` to the output **without calling the field's own `to_representation()`**, so fields never have to handle `None`. 4. Otherwise it calls the field's `to_representation(value)` and stores the result under the field name. ## What the default to_internal_value does 1. It checks that the input is a **mapping**; a list or string is rejected with `Invalid data. Expected a dictionary, but got list.` under the `non_field_errors` key (the name comes from the `NON_FIELD_ERRORS_KEY` setting). 2. It iterates over the **writable** fields, reading each value from the input and running that field's validation, which in turn calls the field's `to_internal_value()`. 3. It collects errors per field instead of stopping at the first, then raises a single `ValidationError` if any occurred. 4. It writes each validated value into the result at the path given by the field's `source`, so a dotted source produces a nested dict. After it returns, the serializer's validators and object-level validation run; those hooks are a separate subject. The final dict is `validated_data`. ## Where the methods sit in a request For a typical create endpoint the order is: 1. The view builds the serializer with `data=request.data`. 2. `is_valid()` calls `run_validation()`, which first handles empty input and then calls **`to_internal_value()`**. 3. Validators and object-level validation run on the result, producing `validated_data`. 4. `save()` passes `validated_data` to `create()` or `update()`. 5. The view returns `serializer.data`, which calls **`to_representation()`** on the saved instance. So the response to a create request is built from the saved object, not echoed from the input: a value normalised on the way in, such as a SKU with surrounding whitespace trimmed by `CharField`, comes back normalised. ## When overriding is the right tool - **Whole-object output changes**: adding a computed key such as `in_stock`, renaming keys for an older client, or removing keys whose value is `None` for a compact catalog feed. Call `super().to_representation(instance)` and edit the dict. - **Accepting an input shape that differs from the output**: a client sends `"price_eur"` while the API field is `price`. Normalise the incoming dict, then call `super().to_internal_value(data)` so field validation still runs. - **A reusable value conversion**: a price that travels as `{"amount": "19.99", "currency": "EUR"}` but is a `Decimal` inside. Write a `serializers.Field` subclass implementing both methods and use it wherever prices appear. ## Doing it safely - **Call `super()`**. Re-implementing the loop loses per-field error collection, `source` handling and read-only filtering. - **Raise `serializers.ValidationError`** from `to_internal_value()`, not `ValueError`; DRF turns the former into a 400 response with field errors, while an unhandled `ValueError` becomes a server error. - **Keep the two directions symmetric** where clients round-trip data, so a representation read from the API can be sent back. - **Remember what tools cannot see**: a key added in `to_representation()` is not a declared field, so schema generation and the browsable API forms do not know about it. A `SerializerMethodField` or a declared field is visible to them. - **Keep business rules out** of conversion methods; checks that compare fields or query the database belong to validation hooks.
- Why call super().to_internal_value() instead of building validated_data yourself in a DRF serializer?The default implementation runs every writable field's validation, collects all field errors into one `ValidationError`, skips read-only fields and writes values at each field's `source` path. Rebuilding it by hand usually loses one of those, for example letting a read-only field be set from input or reporting only the first error.
- When is a SerializerMethodField better than adding a key in to_representation() in DRF?When the value should be part of the declared contract. A `SerializerMethodField` is a real, read-only field, so it appears in `Meta.fields`, in `repr()`, in generated schemas and in the browsable API. A key added in `to_representation()` is invisible to all of them, which suits only presentation tweaks.
saying these in an interview costs you the question
- to_representation() is called during is_valid() to check the input.
- Fields must handle None themselves in to_representation().
- Raising ValueError in to_internal_value() gives the client a 400 with field errors.
- Overriding to_internal_value() without super() still runs every field's validation.
- Keys added in to_representation() appear in the generated API schema.