A Django REST Framework serializer validates received fields and exposes accepted values through validated_data after successful validation.
Django REST Framework serializers: reject coercion outside the wire contract
Operation contract
The receipt serializer declares an exact field set, a six-character ASCII receipt identifier and an integer minor-unit amount. Its custom field rejects text and boolean amounts instead of coercing them into integers. The serializer checks the complete record before the caller reads its accepted data. Serialization itself performs no storage publication.
Failure and ownership boundary
Some built-in fields intentionally accept coercible representations; that may be correct for another protocol but not this one. Unknown fields and duplicate JSON keys need separate policies, and parsed dictionaries cannot recover duplicates already collapsed by a parser. Identity and object permissions also remain separate. Python type conversion: parsing success is not field validity and Django REST Framework object permissions: check the retrieved record, not only login define those boundaries.
Tested environment
Dependency check: this program was executed on CPython 3.14.6 with Django==5.2.17, djangorestframework==3.18.1. Install these versions in a separate virtual environment. The download includes the recorded environment snapshot; no third-party package is part of the website runtime.
Working program
from django.conf import settings
settings.configure(INSTALLED_APPS=[], USE_I18N=False)
import django
django.setup()
from rest_framework import serializers
class MinorAmount(serializers.Field):
def to_internal_value(self, value):
if type(value) is not int or not 0 <= value <= 1000000:
raise serializers.ValidationError("exact bounded integer required")
return value
def to_representation(self, value):
return value
class ReceiptInput(serializers.Serializer):
receipt_id = serializers.RegexField(r"\AR-[0-9]{4}\Z", max_length=6, trim_whitespace=False)
amount = MinorAmount()
def to_internal_value(self, data):
if not isinstance(data, dict) or set(data) != {"receipt_id", "amount"}:
raise serializers.ValidationError({"non_field_errors": ["exact field set required"]})
return super().to_internal_value(data)
accepted = ReceiptInput(data={"receipt_id": "R-0041", "amount": 125})
accepted.is_valid(raise_exception=True)
print(dict(accepted.validated_data))
for amount in (True, "125", -1):
candidate = ReceiptInput(data={"receipt_id": "R-0041", "amount": amount})
print("accepted:", candidate.is_valid())
extra = ReceiptInput(data={"receipt_id": "R-0041", "amount": 125, "owner_id": 41})
print("extra accepted:", extra.is_valid())Output
{'receipt_id': 'R-0041', 'amount': 125}
accepted: False
accepted: False
accepted: False
extra accepted: FalseCosts and limits
Validation scans the selected fields and retains accepted/error state. Bounds on fields do not replace a pre-parse request byte limit. A valid serializer does not imply an indexed query, transaction or permitted record mutation.
Common Mistakes
- Read validated_data only after successful validation.
- Accepting a schema does not prove that the caller owns a receipt.
Connected lessons
Python type conversion: parsing success is not field validity, Python JSON validation: reject duplicate members and non-integer amounts, Django REST Framework object permissions: check the retrieved record, not only login.
Check the next state boundary
Django REST Framework creation ownership: assign the owner from the principal.
