skip to content

In Django REST Framework, what do to_representation() and to_internal_value() do, and when is overriding them the right tool?

level: middleimportance: should knowfreq 48%

answer

  1. two directions of conversion
  2. .data versus is_valid()
  3. call super() and adjust
  4. a custom Field for reuse

basics

~20 s

to_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 s

Every 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 lines
python
from 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 data

go deeper

for a junior

Know that to_representation() produces the output you read from .data and to_internal_value() converts request data during is_valid().

for a middle

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.

for a senior

Choose between a serializer override, a custom Field and a SerializerMethodField, keep round-trips symmetric, and avoid hiding contract changes from schema tools.

for a principal

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.