Typorithm · medium

Serialize the public contract, not every private field

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

Step 1 of 4 · intro card

Serialize the public contract, not every private field

0%

Concept and contract

Use an explicit output type and understand the limits of secret masking.

A successfully validated private object is not necessarily safe to send to another user. It may contain credentials, internal notes or fields belonging to a subclass. The output contract should state which information is public instead of assuming that every checked attribute can be serialized.

The example creates a PrivateUser with a synthetic password field and places it in an Envelope whose user is annotated as PublicUser. The public type contains a name and an optional nickname. This gives the serializer a deliberate public projection rather than relying on a caller to remember an ad hoc exclusion list for every response.

Mechanism and worked example

Pydantic validates the synthetic private input, then serializes the envelope in JSON mode using the declared public field type.

Pydantic validates the synthetic private input, then serializes the envelope in JSON mode using the declared public field type. The password does not appear in the expected response. A defaulted nickname appears as null unless a separate output option removes None values.

The fixture compares exact keys, including the absence of the password key. SecretStr provides masked ordinary representations, but the underlying value still exists and can be retrieved deliberately. Masking is not encryption, secure storage or a substitute for selecting public fields. The lab uses fictional values only and never authenticates an external service.

Complete worked example

python

from pydantic import BaseModel, SecretStr, ValidationError

class PublicUser(BaseModel):
    name: str
    nickname: str | None = None

class PrivateUser(PublicUser):
    password: SecretStr

class Envelope(BaseModel):
    user: PublicUser

def inspect_example(payload):
    try:
        private = PrivateUser.model_validate(payload)
        envelope = Envelope(user=private)
        return {"value": envelope.model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Pydantic validates the synthetic private input, then serializes the envelope in JSON mode using the declared public field type. The password does not appear in the expected response. A defaulted nickname appears as null unless a separate output option removes None values. The fixture compares exact keys, including the absence of the password key. SecretStr provides masked ordinary representations, but the underlying value still exists and can be retrieved deliberately. Masking is not encryption, secure storage or a substitute for selecting public fields. The lab uses fictional values only and never authenticates an external service.

Focused decision: use the complete example for context

python

from pydantic import BaseModel

class PublicUser(BaseModel):
    name: str
    nickname: str | None = None

class Envelope(BaseModel):
    user: PublicUser

def public_payload(user):
    return Envelope(user=user).model_dump(mode="json")
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

Enabling serialize_as_any can make runtime subclass fields appear in output.

Enabling serialize_as_any can make runtime subclass fields appear in output. Even if SecretStr masks the synthetic password value, exposing an unexpected field can still violate the public schema and reveal implementation details. The counterexample changes that serialization option so the acceptance fixture notices an extra key.

Do not treat masked strings as a universal logging policy. Custom serializers, explicit secret extraction or another data structure may expose the real value. Define allowed response models, restrict sensitive logs and test the exact output boundary. Returning a raw framework Response can bypass a service response model, a separate issue revisited in the FastAPI course.

Controlled mistake: observe the actual wrong behavior

python

from pydantic import BaseModel, SecretStr, ValidationError

class PublicUser(BaseModel):
    name: str
    nickname: str | None = None

class PrivateUser(PublicUser):
    password: SecretStr

class Envelope(BaseModel):
    user: PublicUser

def inspect_example(payload):
    try:
        private = PrivateUser.model_validate(payload)
        envelope = Envelope(user=private)
        return {"value": envelope.model_dump(mode="json", serialize_as_any=True)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Enabling serialize_as_any can make runtime subclass fields appear in output. Even if SecretStr masks the synthetic password value, exposing an unexpected field can still violate the public schema and reveal implementation details. The counterexample changes that serialization option so the acceptance fixture notices an extra key. Do not treat masked strings as a universal logging policy. Custom serializers, explicit secret extraction or another data structure may expose the real value. Define allowed response models, restrict sensitive logs and test the exact output boundary. Returning a raw framework Response can bypass a service response model, a separate issue revisited in the FastAPI course.

Independent practice: one bounded change

Omit null-valued public fields while preserving non-null nicknames and the public projection.

Omit null-valued public fields while preserving non-null nicknames and the public projection. Add exclude_none=True to the existing dump. A user with no nickname should now produce only a name; a supplied nickname must remain visible.

Test both shapes and a malformed input. Do not remove nickname from the model or broaden serialization to include the private subclass. The exercise changes a documented output omission policy, not which internal fields are public. Consider what an existing client expects before applying the same change to a real API.

Step 1: Omit null public fields while preserving a non-null nickname and excluding private subclass fields. 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 pydantic import BaseModel, SecretStr, ValidationError

class PublicUser(BaseModel):
    name: str
    nickname: str | None = None

class PrivateUser(PublicUser):
    password: SecretStr

class Envelope(BaseModel):
    user: PublicUser

def inspect_example(payload):
    try:
        private = PrivateUser.model_validate(payload)
        envelope = Envelope(user=private)
        return {"value": envelope.model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Omit null public fields while preserving a non-null nickname and excluding private subclass fields.
Reasoning
Omit null public fields while preserving a non-null nickname and excluding private subclass fields.
  • Hint 1: Keep the Envelope user annotation as PublicUser.
  • Hint 2: Add exclude_none=True to the existing model_dump call.

Solution, tests and trade-offs

The solution retains JSON-mode serialization and adds the requested omission option.

The solution retains JSON-mode serialization and adds the requested omission option. The public output excludes a default None nickname, retains a string nickname and still excludes the private password field. An invalid name remains a validation failure rather than being repaired by serialization.

Output omission changes can affect clients that distinguish a missing field from explicit null. Describe that compatibility decision separately from the privacy projection. The safe core is an explicit public type with tested fields; exclude_none, aliases and formatting then refine its representation without granting access to additional internal state.

Practice solution and acceptance checks

python

from pydantic import BaseModel, SecretStr, ValidationError

class PublicUser(BaseModel):
    name: str
    nickname: str | None = None

class PrivateUser(PublicUser):
    password: SecretStr

class Envelope(BaseModel):
    user: PublicUser

def inspect_example(payload):
    try:
        private = PrivateUser.model_validate(payload)
        envelope = Envelope(user=private)
        return {"value": envelope.model_dump(mode="json", exclude_none=True)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution retains JSON-mode serialization and adds the requested omission option. The public output excludes a default None nickname, retains a string nickname and still excludes the private password field. An invalid name remains a validation failure rather than being repaired by serialization. Output omission changes can affect clients that distinguish a missing field from explicit null. Describe that compatibility decision separately from the privacy projection. The safe core is an explicit public type with tested fields; exclude_none, aliases and formatting then refine its representation without granting access to additional internal state.

Interview reasoning and next step

Explain why a masked value and an absent private field are different guarantees.

Explain why a masked value and an absent private field are different guarantees. Show the public expected mapping and identify a test that would fail if a private subclass field leaked into it. This is a concrete review method for API privacy rather than a promise based on a type name containing Secret.

Continue with JSON Schema, which documents these input and output contracts. A generated schema can describe read-only or serialized fields, but it does not itself apply authentication or prevent a developer from returning a raw object through a different route. Test the actual path that sends data to clients.

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-serialization. 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 an explicit output type and understand the limits of secret masking.