Typorithm · medium

Validation schema and serialization schema describe different directions

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

Step 1 of 4 · intro card

Validation schema and serialization schema describe different directions

0%

Concept and contract

Read generated schemas alongside actual validation and computed output.

JSON Schema documents a data contract for tools and clients. Input validation and output serialization can have different shapes: a computed field may appear only in output, and a Decimal is normally serialized as JSON text to preserve its representation. Ask which direction a schema describes before using it to generate a client.

Our Quote model accepts a positive Decimal amount and computes a doubled total. The validation schema describes the input amount. The serialization schema includes total as a read-only computed field and describes the emitted Decimal representation. The example inspects a few meaningful schema properties rather than relying on the ordering of a large generated document.

Mechanism and worked example

The inspection function validates the input, dumps a JSON-compatible value and obtains both schema modes.

The inspection function validates the input, dumps a JSON-compatible value and obtains both schema modes. It reports whether total appears in the validation schema, whether output total is read-only and the output type of amount. Those observations connect documentation to actual object behavior.

For amount="2.50", the result preserves the decimal scale in the shown JSON strings and includes the calculated total. A zero amount fails its Field bound before a successful quote is returned. Run both the value fixture and the schema observations; checking only an attractive OpenAPI page would miss a runtime validation regression.

Complete worked example

python

from decimal import Decimal
from pydantic import BaseModel, Field, ValidationError, computed_field

class Quote(BaseModel):
    amount: Decimal = Field(gt=0)

    @computed_field
    @property
    def total(self) -> Decimal:
        return self.amount * 2

def inspect_example(payload):
    try:
        quote = Quote.model_validate(payload)
        inputs = Quote.model_json_schema(mode="validation")
        outputs = Quote.model_json_schema(mode="serialization")
        return {"value": quote.model_dump(mode="json"),
                "input_has_total": "total" in inputs["properties"],
                "total_read_only": outputs["properties"]["total"]["readOnly"],
                "amount_output_type": outputs["properties"]["amount"]["type"]}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The inspection function validates the input, dumps a JSON-compatible value and obtains both schema modes. It reports whether total appears in the validation schema, whether output total is read-only and the output type of amount. Those observations connect documentation to actual object behavior. For amount="2.50", the result preserves the decimal scale in the shown JSON strings and includes the calculated total. A zero amount fails its Field bound before a successful quote is returned. Run both the value fixture and the schema observations; checking only an attractive OpenAPI page would miss a runtime validation regression.

Focused decision: use the complete example for context

python

from decimal import Decimal
from pydantic import BaseModel, Field, computed_field

class Quote(BaseModel):
    amount: Decimal = Field(gt=0)

    @computed_field
    @property
    def total(self) -> Decimal:
        return self.amount * 2
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

Adding exclusiveMinimum through json_schema_extra is not the same as enforcing a runtime Field(gt=0) constraint.

Adding exclusiveMinimum through json_schema_extra is not the same as enforcing a runtime Field(gt=0) constraint. Schema-only annotations can describe a rule the actual validator does not apply. The controlled counterexample makes that substitution, so the zero-amount acceptance case catches the disagreement.

A readOnly schema property also does not implement authorization or strip a raw response on its own. A consumer may ignore the schema, and a service can return data through a path that bypasses model serialization. Treat schemas as useful contracts that must be tested against the real request and response boundary, not as an independent security barrier.

Controlled mistake: observe the actual wrong behavior

python

from decimal import Decimal
from pydantic import BaseModel, Field, ValidationError, computed_field

class Quote(BaseModel):
    amount: Decimal = Field(json_schema_extra={"exclusiveMinimum": 0})

    @computed_field
    @property
    def total(self) -> Decimal:
        return self.amount * 2

def inspect_example(payload):
    try:
        quote = Quote.model_validate(payload)
        inputs = Quote.model_json_schema(mode="validation")
        outputs = Quote.model_json_schema(mode="serialization")
        return {"value": quote.model_dump(mode="json"),
                "input_has_total": "total" in inputs["properties"],
                "total_read_only": outputs["properties"]["total"]["readOnly"],
                "amount_output_type": outputs["properties"]["amount"]["type"]}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Adding exclusiveMinimum through json_schema_extra is not the same as enforcing a runtime Field(gt=0) constraint. Schema-only annotations can describe a rule the actual validator does not apply. The controlled counterexample makes that substitution, so the zero-amount acceptance case catches the disagreement. A readOnly schema property also does not implement authorization or strip a raw response on its own. A consumer may ignore the schema, and a service can return data through a path that bypasses model serialization. Treat schemas as useful contracts that must be tested against the real request and response boundary, not as an independent security barrier.

Independent practice: one bounded change

Add a maximum accepted amount of one hundred while retaining the positive lower bound and the computed output.

Add a maximum accepted amount of one hundred while retaining the positive lower bound and the computed output. Keep both schema modes and the JSON serialization method. The task adds one runtime constraint instead of modifying the returned schema dictionary by hand.

Test exactly one hundred, one hundred and one, and zero. Predict the resulting total for the accepted boundary before running the cases. The output type observations should remain unchanged because a numeric bound does not change the model into a different serialization type.

Step 1: Add a runtime maximum amount of 100 while retaining the lower bound and computed serialization. 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 decimal import Decimal
from pydantic import BaseModel, Field, ValidationError, computed_field

class Quote(BaseModel):
    amount: Decimal = Field(gt=0)

    @computed_field
    @property
    def total(self) -> Decimal:
        return self.amount * 2

def inspect_example(payload):
    try:
        quote = Quote.model_validate(payload)
        inputs = Quote.model_json_schema(mode="validation")
        outputs = Quote.model_json_schema(mode="serialization")
        return {"value": quote.model_dump(mode="json"),
                "input_has_total": "total" in inputs["properties"],
                "total_read_only": outputs["properties"]["total"]["readOnly"],
                "amount_output_type": outputs["properties"]["amount"]["type"]}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Add a runtime maximum amount of 100 while retaining the lower bound and computed serialization.
Reasoning
Add a runtime maximum amount of 100 while retaining the lower bound and computed serialization.
  • Hint 1: Change the existing Field declaration, not the generated schema dictionary.
  • Hint 2: Add le=100 beside gt=0.

Solution, tests and trade-offs

The solution uses Field(gt=0, le=100).

The solution uses Field(gt=0, le=100). A quote for one hundred validates and computes a total of two hundred, while one hundred and one is rejected by the upper bound. Zero is still rejected by the original lower bound, demonstrating that the practice change did not replace an existing invariant.

Generated schema details can evolve between library versions. The fixture therefore asserts meaningful properties and actual accepted values instead of treating whitespace or property order as a behavioral guarantee. Recheck important schema changes during dependency upgrades, especially when a generated client or validator in another language depends on them.

Practice solution and acceptance checks

python

from decimal import Decimal
from pydantic import BaseModel, Field, ValidationError, computed_field

class Quote(BaseModel):
    amount: Decimal = Field(gt=0, le=100)

    @computed_field
    @property
    def total(self) -> Decimal:
        return self.amount * 2

def inspect_example(payload):
    try:
        quote = Quote.model_validate(payload)
        inputs = Quote.model_json_schema(mode="validation")
        outputs = Quote.model_json_schema(mode="serialization")
        return {"value": quote.model_dump(mode="json"),
                "input_has_total": "total" in inputs["properties"],
                "total_read_only": outputs["properties"]["total"]["readOnly"],
                "amount_output_type": outputs["properties"]["amount"]["type"]}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution uses Field(gt=0, le=100). A quote for one hundred validates and computes a total of two hundred, while one hundred and one is rejected by the upper bound. Zero is still rejected by the original lower bound, demonstrating that the practice change did not replace an existing invariant. Generated schema details can evolve between library versions. The fixture therefore asserts meaningful properties and actual accepted values instead of treating whitespace or property order as a behavioral guarantee. Recheck important schema changes during dependency upgrades, especially when a generated client or validator in another language depends on them.

Interview reasoning and next step

In an interview, explain an input-only, output-only and shared field, then name which schema mode exposes each one.

In an interview, explain an input-only, output-only and shared field, then name which schema mode exposes each one. Contrast a runtime constraint with a schema annotation using the rejected zero example. This links modelling to observable API behavior rather than just documentation generation.

The next topic examines trusted copy operations. A model can have an excellent schema and still be placed into an invalid state through an unchecked update. Preserve the relationship between the contract, the actual validator and every path that creates or changes a model instance.

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

Read generated schemas alongside actual validation and computed output.