Typorithm · medium

Strict Python input and strict JSON are different channels

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

Step 1 of 4 · intro card

Strict Python input and strict JSON are different channels

0%

Concept and contract

Use strict validation deliberately and test both input representations.

Strict validation reduces automatic conversions, but its behavior also depends on the input channel and the type. JSON has no native date object, so a strict JSON date field can accept an ISO date string. The same string presented as a Python value is not already a date and can be rejected.

The example uses one Plan model with strict=True and two explicit entry paths. A JSON path uses model_validate_json. A Python path uses model_validate on the mapping. Treating those methods as interchangeable loses an important part of the contract, especially when values such as dates and UUIDs cross a JSON boundary.

Mechanism and worked example

The fixture labels the chosen mode and passes the same conceptual payload through each channel.

The fixture labels the chosen mode and passes the same conceptual payload through each channel. In JSON mode, the ISO date is interpreted using the JSON date rules and the result is dumped back into JSON-compatible values. In Python mode, that date string fails the strict date field.

The integer count remains strict in both modes: the string "2" and boolean true are not an integer count. Inspect the successful date conversion together with those rejected integer representations. This makes it clear why strict does not mean every JSON string is rejected or every vaguely numeric value is accepted.

Complete worked example

python

import json
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Plan(BaseModel):
    model_config = ConfigDict(strict=True)
    day: date
    count: int

def inspect_example(request):
    value = request["value"]
    try:
        if request["mode"] == "json":
            plan = Plan.model_validate_json(json.dumps(value))
        else:
            plan = Plan.model_validate(value)
        return {"value": plan.model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The fixture labels the chosen mode and passes the same conceptual payload through each channel. In JSON mode, the ISO date is interpreted using the JSON date rules and the result is dumped back into JSON-compatible values. In Python mode, that date string fails the strict date field. The integer count remains strict in both modes: the string "2" and boolean true are not an integer count. Inspect the successful date conversion together with those rejected integer representations. This makes it clear why strict does not mean every JSON string is rejected or every vaguely numeric value is accepted.

Focused decision: use the complete example for context

python

from datetime import date
from pydantic import BaseModel, ConfigDict

class Plan(BaseModel):
    model_config = ConfigDict(strict=True)
    day: date
    count: int

def parse_json(body):
    return Plan.model_validate_json(body)
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

Decoding JSON into a mapping first and then calling strict model_validate changes the input channel.

Decoding JSON into a mapping first and then calling strict model_validate changes the input channel. If the service promised strict JSON handling, this apparently harmless refactor can reject previously valid date input. The controlled counterexample makes exactly that substitution so a real acceptance case catches the regression.

Do not switch strictness off globally merely to make a failing fixture pass. Decide which fields allow conversion and which channel the boundary actually receives. Field-level strict options can be appropriate, but they need their own input examples. Also avoid claiming that strictness establishes units, timezone policy, permissions or business limits.

Controlled mistake: observe the actual wrong behavior

python

import json
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Plan(BaseModel):
    model_config = ConfigDict(strict=True)
    day: date
    count: int

def inspect_example(request):
    value = request["value"]
    try:
        if request["mode"] == "json":
            plan = Plan.model_validate(json.loads(json.dumps(value)))
        else:
            plan = Plan.model_validate(value)
        return {"value": plan.model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Decoding JSON into a mapping first and then calling strict model_validate changes the input channel. If the service promised strict JSON handling, this apparently harmless refactor can reject previously valid date input. The controlled counterexample makes exactly that substitution so a real acceptance case catches the regression. Do not switch strictness off globally merely to make a failing fixture pass. Decide which fields allow conversion and which channel the boundary actually receives. Field-level strict options can be appropriate, but they need their own input examples. Also avoid claiming that strictness establishes units, timezone policy, permissions or business limits.

Independent practice: one bounded change

Keep both entry paths and require count to be at least one.

Keep both entry paths and require count to be at least one. A zero count in valid JSON must now fail the lower bound, while the ISO JSON date remains accepted. Numeric text must still fail the strict integer contract.

Add the bound to the existing count field instead of rewriting the date handling. Run a positive JSON example, zero count, and a strict Python date-string example. This isolates the intended business-policy change from the channel behavior that the lesson is designed to preserve.

Step 1: Require count >= 1 without changing strictness or the JSON validation channel. 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

import json
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Plan(BaseModel):
    model_config = ConfigDict(strict=True)
    day: date
    count: int

def inspect_example(request):
    value = request["value"]
    try:
        if request["mode"] == "json":
            plan = Plan.model_validate_json(json.dumps(value))
        else:
            plan = Plan.model_validate(value)
        return {"value": plan.model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Require count >= 1 without changing strictness or the JSON validation channel.
Reasoning
Require count >= 1 without changing strictness or the JSON validation channel.
  • Hint 1: Keep ConfigDict(strict=True) and both existing methods.
  • Hint 2: Field is already imported; add ge=1 to count.

Solution, tests and trade-offs

The solution attaches Field(ge=1) to count while keeping ConfigDict(strict=True).

The solution attaches Field(ge=1) to count while keeping ConfigDict(strict=True). A valid integer remains an integer and a valid JSON date remains serializable as its ISO representation. The tests verify error types for the zero bound and for the wrong Python date representation independently.

Validation and serialization are different directions. Dumping a validated date as a string for JSON does not mean that every Python string should be accepted as that date later. Document the method at an API boundary, and preserve it when moving validation into a helper or a dependency. Method choice can change the accepted input set.

Practice solution and acceptance checks

python

import json
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Plan(BaseModel):
    model_config = ConfigDict(strict=True)
    day: date
    count: int = Field(ge=1)

def inspect_example(request):
    value = request["value"]
    try:
        if request["mode"] == "json":
            plan = Plan.model_validate_json(json.dumps(value))
        else:
            plan = Plan.model_validate(value)
        return {"value": plan.model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution attaches Field(ge=1) to count while keeping ConfigDict(strict=True). A valid integer remains an integer and a valid JSON date remains serializable as its ISO representation. The tests verify error types for the zero bound and for the wrong Python date representation independently. Validation and serialization are different directions. Dumping a validated date as a string for JSON does not mean that every Python string should be accepted as that date later. Document the method at an API boundary, and preserve it when moving validation into a helper or a dependency. Method choice can change the accepted input set.

Interview reasoning and next step

For an interview, explain why the two calls can produce different answers without either being a bug.

For an interview, explain why the two calls can produce different answers without either being a bug. Name the missing date type in JSON, then contrast it with the real types a Python caller can supply. Use the count examples to demonstrate that the exception is type-specific, not unrestricted coercion.

Continue with validators after the channel policy is explicit. Custom validation should fill a real domain gap rather than trying to undo an undocumented conversion. When testing a refactor, include the representation that a real client sends, not only manually constructed Python objects that skip the serialization boundary.

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/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track pydantic, then edit the working exercise baseline and run --practice pyd-strict. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Use strict validation deliberately and test both input representations.