Typorithm · medium

Validate OpenAPI and the actual response together

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

Step 1 of 4 · intro card

Validate OpenAPI and the actual response together

0%

Concept and contract

Use the recommended schema tool without mistaking documentation for runtime enforcement.

An OpenAPI document helps clients understand routes and representations, but its declarations do not execute the endpoint. A useful contract test checks the generated schema and actual emitted response. A privacy regression can leave documentation looking perfectly reasonable.

The fixture uses drf-spectacular, which DRF documentation recommends for richer schema generation as its built-in generator is deprecated. It generates a schema from an owned ListAPIView and validates the OpenAPI document using the installed local package data. No remote schema service or production endpoint is contacted.

Mechanism and worked example

The public serializer declares id, title and country.

The public serializer declares id, title and country. SchemaGenerator inspects that contract for the /roles/ GET operation. The same generic view serializes a real owned country selection, allowing the test to compare both the component properties and returned rows.

Schema validation checks the document format, while the response assertions check application behavior. Neither test substitutes for the other. The owned view has a deliberately simple fixture principal; a real public schema must not enumerate private user data while generating documentation.

Complete worked example

python

from drf_lab import setup
setup()

from django.urls import path
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.validation import validate_schema
from rest_framework import generics, serializers
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class RoleList(generics.ListAPIView):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice", country=self.request.query_params.get("country", "UA")).order_by("id")

urlpatterns = [path("roles/", RoleList.as_view())]

def inspect_example(country):
    reset_fixture()
    document = SchemaGenerator(patterns=urlpatterns).get_schema(request=None, public=True)
    validate_schema(document)
    properties = document["components"]["schemas"]["PublicRole"]["properties"]
    response = RoleList.as_view()(APIRequestFactory().get("/roles/", {"country": country}))
    return {"properties": sorted(properties), "get_documented": "get" in document["paths"]["/roles/"],
            "status": response.status_code, "rows": list(response.data)}
Reasoning
The public serializer declares id, title and country. SchemaGenerator inspects that contract for the /roles/ GET operation. The same generic view serializes a real owned country selection, allowing the test to compare both the component properties and returned rows. Schema validation checks the document format, while the response assertions check application behavior. Neither test substitutes for the other. The owned view has a deliberately simple fixture principal; a real public schema must not enumerate private user data while generating documentation.

Focused decision: context is in the complete example

python

document = SchemaGenerator(patterns=urlpatterns).get_schema(request=None, public=True)
validate_schema(document)
response = RoleList.as_view()(APIRequestFactory().get("/roles/", {"country": "UA"}))
actual_rows = list(response.data)
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 overrides list to return raw selected values including private_note.

The counterexample overrides list to return raw selected values including private_note. The declared serializer and generated schema remain public-looking, but the actual response leaks the internal canary. A schema snapshot alone would miss that bypass.

Do not repair a response test by deserializing it through the public serializer before comparison: that can discard the leaked field and hide the bug. Compare actual emitted keys or complete bodies. Also review generated operation names and request/response distinctions rather than accepting every schema diff automatically.

Controlled mistake: observe the actual wrong behavior

python

from drf_lab import setup
setup()

from django.urls import path
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.validation import validate_schema
from rest_framework import generics, serializers
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class RoleList(generics.ListAPIView):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice", country=self.request.query_params.get("country", "UA")).order_by("id")

def leaky_list(self, request, *args, **kwargs):
    return Response(list(self.get_queryset().values("id", "title", "country", "private_note")))
RoleList.list = leaky_list
urlpatterns = [path("roles/", RoleList.as_view())]

def inspect_example(country):
    reset_fixture()
    document = SchemaGenerator(patterns=urlpatterns).get_schema(request=None, public=True)
    validate_schema(document)
    properties = document["components"]["schemas"]["PublicRole"]["properties"]
    response = RoleList.as_view()(APIRequestFactory().get("/roles/", {"country": country}))
    return {"properties": sorted(properties), "get_documented": "get" in document["paths"]["/roles/"],
            "status": response.status_code, "rows": list(response.data)}
Reasoning
The counterexample overrides list to return raw selected values including private_note. The declared serializer and generated schema remain public-looking, but the actual response leaks the internal canary. A schema snapshot alone would miss that bypass. Do not repair a response test by deserializing it through the public serializer before comparison: that can discard the leaked field and hide the bug. Compare actual emitted keys or complete bodies. Also review generated operation names and request/response distinctions rather than accepting every schema diff automatically.

Independent practice

Add slots to the declared public representation and verify it appears in both the schema component and actual rows.

Add slots to the declared public representation and verify it appears in both the schema component and actual rows. Preserve owner selection and keep private_note absent. This is a deliberate small contract expansion, not permission to return all model fields.

Check Ukrainian, Israeli and empty United States selections. The component should describe slots even when a response has no rows. Do not fabricate the property in the test helper; the serializer must own the new representation.

Step 1: Expose slots through the serializer so schema and actual responses expand together without private-field leakage. 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 django.urls import path
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.validation import validate_schema
from rest_framework import generics, serializers
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class RoleList(generics.ListAPIView):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice", country=self.request.query_params.get("country", "UA")).order_by("id")

urlpatterns = [path("roles/", RoleList.as_view())]

def inspect_example(country):
    reset_fixture()
    document = SchemaGenerator(patterns=urlpatterns).get_schema(request=None, public=True)
    validate_schema(document)
    properties = document["components"]["schemas"]["PublicRole"]["properties"]
    response = RoleList.as_view()(APIRequestFactory().get("/roles/", {"country": country}))
    return {"properties": sorted(properties), "get_documented": "get" in document["paths"]["/roles/"],
            "status": response.status_code, "rows": list(response.data)}

# Independent task: Expose slots through the serializer so schema and actual responses expand together without private-field leakage.
Reasoning
Expose slots through the serializer so schema and actual responses expand together without private-field leakage.
  • 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 extends the explicit serializer field list.

The solution extends the explicit serializer field list. Both schema generation and generic serialization see the same declared field, while the tests still observe actual responses independently. The document remains valid OpenAPI under the installed schema validator.

Adding a response field can affect strict clients or generated SDKs. Review compatibility and versioning as needed. Documentation may describe protected operations, but permissions, ownership and input validation still need execution tests. A valid schema cannot enforce them by itself.

Practice solution and acceptance checks

python

from drf_lab import setup
setup()

from django.urls import path
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.validation import validate_schema
from rest_framework import generics, serializers
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country", "slots"]
class RoleList(generics.ListAPIView):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice", country=self.request.query_params.get("country", "UA")).order_by("id")

urlpatterns = [path("roles/", RoleList.as_view())]

def inspect_example(country):
    reset_fixture()
    document = SchemaGenerator(patterns=urlpatterns).get_schema(request=None, public=True)
    validate_schema(document)
    properties = document["components"]["schemas"]["PublicRole"]["properties"]
    response = RoleList.as_view()(APIRequestFactory().get("/roles/", {"country": country}))
    return {"properties": sorted(properties), "get_documented": "get" in document["paths"]["/roles/"],
            "status": response.status_code, "rows": list(response.data)}
Reasoning
The solution extends the explicit serializer field list. Both schema generation and generic serialization see the same declared field, while the tests still observe actual responses independently. The document remains valid OpenAPI under the installed schema validator. Adding a response field can affect strict clients or generated SDKs. Review compatibility and versioning as needed. Documentation may describe protected operations, but permissions, ownership and input validation still need execution tests. A valid schema cannot enforce them by itself.

Interview reasoning and next step

Explain the difference between validating an OpenAPI document and validating an endpoint response.

Explain the difference between validating an OpenAPI document and validating an endpoint response. Use the raw-response counterexample to show why both are necessary. Then identify the public field declaration that intentionally expands the contract.

The capstone combines these boundaries in an owned collection and creation API. Carry forward the habit of testing selection, representation, validation and storage effects separately instead of relying on a single successful request.

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-schema-tests. 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.

Use the recommended schema tool without mistaking documentation for runtime enforcement.