Automatically prune sensitive data from inputs and outputs based on granular, context-aware security policies.
The Challenge
Modern APIs often deal with "Multifaceted Data"—where the same record (e.g., a User Profile) has different visible fields depending on who is asking.
1. The Public should only see username and avatar.
2. The Owner should also see email and billing_address.
3. The Admin should see everything, including internal_notes and risk_score.
In standard FastAPI, this usually forces developers to create a separate Pydantic model for every role (UserPublic, UserPrivate, UserAdmin) and write complex if/else logic in routers to choose the correct model. This is error-prone and causes "Schema Explosion."
The ZCore Elegance
ZCore's Zchema allows you to define one model for all roles. By setting the __model__ attribute, the schema becomes "context-aware." When the request starts, your security middleware populates a list of restricted_fields (e.g., ["users.internal_notes"]). ZCore then automatically strips these fields from incoming JSON payloads (Mass Assignment protection) and outgoing JSON responses (Leakage protection).
from zcore import Zchema, request_context
class UserSchema(Zchema):
__model__ = "users" # Ties to the 'users' domain
username: str
email: str
internal_notes: str | None = None
# Inside a middleware or dependency:
restricted = ["users.internal_notes"]
if not user.is_admin:
with request_context(user_id=user.id, fields=restricted):
# Outgoing response now automatically
# masks 'internal_notes'
return UserSchema.model_validate(user_record)
# Multiple models required
class UserPublic(BaseModel):
username: str
class UserPrivate(UserPublic):
email: str
internal_notes: str
@router.get("/users/{id}")
async def get_user(id: uuid.UUID, user: User = Depends(get_user)):
record = await db.get(id)
# Manual role-based branching logic
if user.is_admin:
return UserPrivate.model_validate(record)
return UserPublic.model_validate(record)
Boundaries & Integration
ZCore's masking logic is a non-destructive layer built on top of Pydantic.
- Pydantic Native: Pruning happens during Pydantic's internal
model_validatorandmodel_serializerphases [web/projection.py]. Your logic inside the service or repository layers remains untouched—you always work with full objects. - Opt-In Masking: If a schema does not define
__model__, ZCore ignores it. It behaves like a standardBaseModel. - Freedom to Bypass: If you need to force-send a restricted field for a specific edge case, you can use a standard
pydantic.BaseModelfor that specific route, which will bypass ZCore's context-aware pruning.
Under-the-Hood Spec
1. The Recursive _prune_data Logic
ZCore doesn't just check top-level keys. The internal _prune_data method recursively traverses nested dictionaries and lists [web/projection.py]. If you restrict orders.items.cost, ZCore will find every cost field inside the items list of an Order object and remove it, regardless of the nesting depth.
2. Dynamic JSON-Schema Pruning
When expose_schemas is enabled on a router, ZCore overrides __get_pydantic_json_schema__ [web/projection.py]. It doesn't just mask the data; it masks the documentation. If a user lacks permission to see internal_notes, that field will not appear in the JSON-Schema returned by ?schema=true, allowing your frontend to dynamically hide form inputs.
3. Cache-Safe Vary Headers
Because the same URL can return different JSON content based on the user's permissions, ZCore's ZCoreAPIRoute automatically appends Authorization or Cookie to the Vary HTTP header [web/api_router.py]. This ensures that downstream CDNs or browser caches do not accidentally serve a cached "Admin" response to a "Public" user.
Security Policy
Restricted fields are defined as dot-paths (e.g., model_name.field_name). To restrict an entire model, you can use the model name itself (e.g., users). This will cause the entire schema to return an empty object if the user is unauthorized.