Typorithm · medium

ModelSerializer does not replace a writable-field policy

pythondjangodrfindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

ModelSerializer does not replace a writable-field policy

0%

Concept and contract

Persist a validated owned row and assign ownership on the server.

ModelSerializer derives useful defaults from a Django model, but a storage model and a public writable contract are not the same thing. Explicit fields let a reviewer see what the API accepts and emits. Server-controlled ownership should not be chosen by an arbitrary request field.

The owned lab has real Company and Role tables in SQLite memory. RoleWrite exposes a small field set and a positive bounded slots override. After validation, save receives the trusted fixture owner and company as server-supplied attributes. No live employer or user data is involved.

Mechanism and worked example

The serializer checks field values, then its generated create path constructs a real model row.

The serializer checks field values, then its generated create path constructs a real model row. The helper reports the public key set, selected values, stored owner and row count. Those observations distinguish accepted input from a save that actually persisted the intended state.

The fixture resets its own in-memory rows before every case. IDs in newly created rows are deliberately not assumed to restart after deletion. Public keys still include id, while the test focuses on stable values and ownership rather than a fragile auto-increment expectation.

Complete worked example

python

from drf_lab import setup
setup()

from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class RoleWrite(serializers.ModelSerializer):
    slots = serializers.IntegerField(min_value=1, max_value=5)
    class Meta:
        model = Role
        fields = ["id", "title", "country", "slots"]
        read_only_fields = ["id"]

def inspect_example(payload):
    alice, bob, company = reset_fixture()
    serializer = RoleWrite(data=payload)
    if not serializer.is_valid():
        return {"errors": {key: [e.code for e in values] for key, values in serializer.errors.items()},
                "rows": Role.objects.count()}
    row = serializer.save(owner=alice, company=company)
    return {"value": {key: serializer.data[key] for key in ["title", "country", "slots"]},
            "keys": sorted(serializer.data), "owner": row.owner.username, "rows": Role.objects.count()}
Reasoning
The serializer checks field values, then its generated create path constructs a real model row. The helper reports the public key set, selected values, stored owner and row count. Those observations distinguish accepted input from a save that actually persisted the intended state. The fixture resets its own in-memory rows before every case. IDs in newly created rows are deliberately not assumed to restart after deletion. Public keys still include id, while the test focuses on stable values and ownership rather than a fragile auto-increment expectation.

Focused decision: context is in the complete example

python

class RoleWrite(serializers.ModelSerializer):
    slots = serializers.IntegerField(min_value=1, max_value=5)
    class Meta:
        model = Role
        fields = ["id", "title", "country", "slots"]
        read_only_fields = ["id"]

serializer.is_valid(raise_exception=True)
row = serializer.save(owner=alice, company=company)
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample supplies bob as the owner at save time.

The counterexample supplies bob as the owner at save time. Every ordinary field can remain valid while the row is stored under the wrong principal. Validation success alone cannot catch a service that passes the wrong trusted context.

Avoid fields="__all__" as an unreviewed public contract: later model fields can then change the API surface. Read-only inputs may be ignored rather than rejected, so never treat an ignored owner field as an authorization audit. Check the persisted owner and actual emitted keys.

Controlled mistake: observe the actual wrong behavior

python

from drf_lab import setup
setup()

from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class RoleWrite(serializers.ModelSerializer):
    slots = serializers.IntegerField(min_value=1, max_value=5)
    class Meta:
        model = Role
        fields = ["id", "title", "country", "slots"]
        read_only_fields = ["id"]

def inspect_example(payload):
    alice, bob, company = reset_fixture()
    serializer = RoleWrite(data=payload)
    if not serializer.is_valid():
        return {"errors": {key: [e.code for e in values] for key, values in serializer.errors.items()},
                "rows": Role.objects.count()}
    row = serializer.save(owner=bob, company=company)
    return {"value": {key: serializer.data[key] for key in ["title", "country", "slots"]},
            "keys": sorted(serializer.data), "owner": row.owner.username, "rows": Role.objects.count()}
Reasoning
The counterexample supplies bob as the owner at save time. Every ordinary field can remain valid while the row is stored under the wrong principal. Validation success alone cannot catch a service that passes the wrong trusted context. Avoid fields="__all__" as an unreviewed public contract: later model fields can then change the API surface. Read-only inputs may be ignored rather than rejected, so never treat an ignored owner field as an authorization audit. Check the persisted owner and actual emitted keys.

Independent practice

Reduce the maximum accepted slots value from five to three.

Reduce the maximum accepted slots value from five to three. Keep positive counts, country choices, explicit fields and server-supplied owner/company. An excessive request must fail before a row is saved.

Test three as accepted, four as rejected and an unsupported country as rejected. The rejected cases should leave the four seeded rows intact. Do not merely clamp a submitted value or hide an extra inserted row in the helper; the serializer must implement the new boundary.

Step 1: Allow at most three slots and ensure rejected input creates no row. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from drf_lab import setup
setup()

from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class RoleWrite(serializers.ModelSerializer):
    slots = serializers.IntegerField(min_value=1, max_value=5)
    class Meta:
        model = Role
        fields = ["id", "title", "country", "slots"]
        read_only_fields = ["id"]

def inspect_example(payload):
    alice, bob, company = reset_fixture()
    serializer = RoleWrite(data=payload)
    if not serializer.is_valid():
        return {"errors": {key: [e.code for e in values] for key, values in serializer.errors.items()},
                "rows": Role.objects.count()}
    row = serializer.save(owner=alice, company=company)
    return {"value": {key: serializer.data[key] for key in ["title", "country", "slots"]},
            "keys": sorted(serializer.data), "owner": row.owner.username, "rows": Role.objects.count()}

# Independent task: Allow at most three slots and ensure rejected input creates no row.
Reasoning
Allow at most three slots and ensure rejected input creates no row.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution changes the serializer bound while preserving the generated save flow.

The solution changes the serializer bound while preserving the generated save flow. Successful data is persisted under alice and the public representation excludes private_note and owner. Invalid input returns field error codes with no additional row.

Database constraints remain necessary for races and callers that bypass the serializer. A model-backed API also needs transactions, object access checks and a policy for concurrent updates. This example demonstrates a narrow persistence contract rather than complete production readiness.

Practice solution and acceptance checks

python

from drf_lab import setup
setup()

from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class RoleWrite(serializers.ModelSerializer):
    slots = serializers.IntegerField(min_value=1, max_value=3)
    class Meta:
        model = Role
        fields = ["id", "title", "country", "slots"]
        read_only_fields = ["id"]

def inspect_example(payload):
    alice, bob, company = reset_fixture()
    serializer = RoleWrite(data=payload)
    if not serializer.is_valid():
        return {"errors": {key: [e.code for e in values] for key, values in serializer.errors.items()},
                "rows": Role.objects.count()}
    row = serializer.save(owner=alice, company=company)
    return {"value": {key: serializer.data[key] for key in ["title", "country", "slots"]},
            "keys": sorted(serializer.data), "owner": row.owner.username, "rows": Role.objects.count()}
Reasoning
The solution changes the serializer bound while preserving the generated save flow. Successful data is persisted under alice and the public representation excludes private_note and owner. Invalid input returns field error codes with no additional row. Database constraints remain necessary for races and callers that bypass the serializer. A model-backed API also needs transactions, object access checks and a policy for concurrent updates. This example demonstrates a narrow persistence contract rather than complete production readiness.

Interview reasoning and next step

Explain which behavior ModelSerializer generated and which policy remained explicit: the field set, slots bound and trusted save attributes.

Explain which behavior ModelSerializer generated and which policy remained explicit: the field set, slots bound and trusted save attributes. Show how the stored-owner assertion catches a bug that a simple is_valid test would miss.

Next, use generic views to connect serializers and querysets to HTTP operations. Automatic behavior is helpful only when the selected collection is scoped correctly and unsupported methods remain visible.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/drf-lab-20261105.zip. Install pinned dependencies in an isolated Python environment. Run verify.py, then change the working exercise baseline and run --practice drf-models. The real DRF clients, serializers, schemas and SQLite transactions use owned fixtures only. No production database, feed, account or paid service is used. This lab executes Python; the PC typing trainer and native reading progress do not execute it.

Persist a validated owned row and assign ownership on the server.