Field-level visibility control for Pydantic models
Project description
Okay, let's update the README to accurately reflect the current state, including the capabilities and the important limitation regarding module-level dynamic model creation for documentation purposes.
# Pydantic Visible Fields
A flexible field-level visibility control system for Pydantic models. This library allows you to define which fields of your models are visible to different user roles, making it easy to implement role-based access control at the data model level, especially for API responses.
[](https://www.python.org/downloads/)
[](https://docs.pydantic.dev/)
[](LICENSE)
[](https://badge.fury.io/py/pydantic-visible-fields)
[](https://pepy.tech/project/pydantic-visible-fields)
## Overview
`pydantic-visible-fields` provides a simple way to control which fields of your Pydantic models are included when generating responses, based on user roles. It dynamically creates filtered Pydantic models at runtime, ensuring type safety for the visible data.
It also provides a `PaginatedResponse` class which makes it easy to generate paginated API responses with automatic conversion of objects to the correct visibility level.
### Key Features
- **Field-level visibility control** using a simple `field` decorator.
- **Role inheritance** support to define hierarchical permissions.
- **Nested model support** with full recursive visibility filtering.
- **Circular reference handling** replacing cycles with `None` in the output.
- **Pydantic V2 Compatible** leveraging modern Pydantic features.
- **Simple integration** with FastAPI and other web frameworks via a helper function.
## Installation
```bash
pip install pydantic-visible-fields
Basic Usage
1. Define Your Roles
First, define your application's roles using Python's Enum:
from enum import Enum
class Role(str, Enum):
VIEWER = "viewer"
EDITOR = "editor"
ADMIN = "admin"
2. Configure Role System
Configure the role system once, usually at application startup. Define inheritance (e.g., ADMIN inherits EDITOR's visibility) and an optional default role.
from pydantic_visible_fields import configure_roles
configure_roles(
role_enum=Role,
inheritance={
Role.ADMIN: [Role.EDITOR], # Admin sees what Editor sees
Role.EDITOR: [Role.VIEWER], # Editor sees what Viewer sees
},
default_role=Role.VIEWER # Role used if none is specified
)
3. Create Models with Visibility Rules
Inherit from VisibleFieldsModel and use the custom field function to specify which roles can see each field via the visible_to argument.
from pydantic_visible_fields import VisibleFieldsModel, field
class User(VisibleFieldsModel):
id: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
username: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
email: str = field(visible_to=[Role.EDITOR, Role.ADMIN]) # Only Editor and Admin
hashed_password: str = field(visible_to=[Role.ADMIN]) # Only Admin
is_active: bool = field(visible_to=[Role.ADMIN]) # Only Admin
4. Use in API Responses (Runtime Filtering)
In your API endpoint or application logic, use the visible_fields_response helper function to convert your model instance into a role-specific response just before returning it.
from fastapi import FastAPI, Depends
from typing import Any # Use Any for dynamic return type
from pydantic_visible_fields import visible_fields_response
# Assume User model, Role enum, configure_roles, get_user_by_id, get_current_user_role exist
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: str, current_user_role: Role = Depends(get_current_user_role)) -> Any:
user: User = await get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Convert the full user model to a filtered response based on the role
# This dynamically generates and validates the correct response model instance.
response_data = visible_fields_response(user, role=current_user_role)
return response_data
This approach ensures that complex models with forward references or cycles work reliably, as the response model generation happens at runtime when all types are defined.
Note on FastAPI Documentation: When using visible_fields_response, the return type hint in your endpoint (-> Any) won't allow FastAPI to generate precise OpenAPI documentation for the different role-specific response schemas. See the "FastAPI Integration & Documentation" section for strategies to improve documentation if needed.
Advanced Usage
Class-Level Visibility (Alternative)
Instead of decorating each field, you can define visibility rules at the class level using the VisibleFieldsMixin and a _role_visible_fields class variable. Note that field-level visible_to takes precedence if both are used.
from pydantic import BaseModel
from pydantic_visible_fields import VisibleFieldsMixin
from typing import ClassVar, Dict, Set
class UserSettings(BaseModel, VisibleFieldsMixin):
# Map role *string values* to sets of visible field names
_role_visible_fields: ClassVar[Dict[str, Set[str]]] = {
Role.VIEWER.value: {"id", "theme"},
Role.EDITOR.value: {"notifications"}, # Editor also inherits Viewer's fields
Role.ADMIN.value: {"advanced_options", "debug_mode"}, # Admin inherits Editor/Viewer
}
id: str
theme: str
notifications: bool
advanced_options: dict
debug_mode: bool
# Need to explicitly call model_rebuild if using ForwardRefs within the class
model_rebuild()
Nested Models
Visibility filtering automatically applies recursively to nested models that also inherit from VisibleFieldsModel or VisibleFieldsMixin.
class Address(VisibleFieldsModel):
street: str = field(visible_to=[Role.EDITOR, Role.ADMIN])
city: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
country: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
postal_code: str = field(visible_to=[Role.EDITOR, Role.ADMIN])
class FullUser(VisibleFieldsModel):
id: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
# ... other user fields ...
# The address field itself is visible to all, but the *content* of the
# Address object will be filtered based on the role when processed.
address: Address = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
A Viewer requesting a FullUser would see the address field containing an Address object with only the city and country fields populated.
Working with Collections
Visibility filtering also works automatically for models within List and Dict structures.
from typing import List, Dict
# Assume User and Setting models are defined using VisibleFieldsModel/Mixin
class Team(VisibleFieldsModel):
id: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
name: str = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
# Each User object in this list will be filtered based on the role
members: List[User] = field(visible_to=[Role.VIEWER, Role.EDITOR, Role.ADMIN])
# Each Setting object in this dict's values will be filtered
settings: Dict[str, Setting] = field(visible_to=[Role.EDITOR, Role.ADMIN])
Circular References
The library handles circular object references safely. When converting to a dictionary (visible_dict) or response model (to_response_model), cycles are detected, and the reference causing the loop is replaced with None in the final output object instance if the field type allows it (e.g., Optional[...]).
Dynamic Visibility Configuration
You can dynamically update visibility rules for a class after initial definition using configure_visibility. Note: This modifies the class directly and is generally not thread-safe. It also clears the internal cache for response models associated with that class.
# Make 'email' visible to VIEWER role for the User class dynamically
User.configure_visibility(Role.VIEWER, {"id", "username", "email"})
# Overwrite all fields visible to ADMIN
User.configure_visibility(Role.ADMIN, {"id", "username", "email", "hashed_password", "is_active"})
FastAPI Integration & Documentation
The recommended way to use this library with FastAPI is to use the visible_fields_response helper function within your endpoint logic before returning the data, as shown in the Basic Usage example.
Challenge: FastAPI generates OpenAPI documentation based on static type hints provided in the response_model parameter of route decorators. Since the actual response model type generated by visible_fields_response depends on the runtime role, using a static hint like response_model=User will show the full model in docs, while response_model=Any provides no schema detail.
Strategies for Documentation:
- Accept Less Specific Docs: Use
response_model=Anyorresponse_model=YourBaseModelin the decorator and rely on thesummary/descriptionfields or manually added OpenAPIresponsesinformation to clarify the different role-based outputs. Runtime filtering still works correctly. - Manually Define Response Models: For endpoints where precise documentation is critical, manually define the specific response model variations (e.g.,
UserAdminResponse,UserUserResponse) in your code. Use these static models in theresponse_modeldecorator. You can still usevisible_fields_responseat runtime for consistent filtering logic, or map data manually. This duplicates effort but gives accurate static docs.
Module-Level Creation Limitation:
Due to limitations in Python's import system and Pydantic's Forward Reference resolution, calling YourModel.create_response_model(role) directly at the module level is strongly discouraged and likely to fail if your models involve complex forward references or cyclic dependencies (e.g., Optional["MyUnion"] where MyUnion is defined later or contains forward references). Please use the runtime visible_fields_response approach instead.
API Reference
Core Classes
VisibleFieldsModel: Inherit from this instead ofpydantic.BaseModelto use thefielddecorator for visibility.VisibleFieldsMixin: Mixin to add visibility features to existingpydantic.BaseModelclasses (use with class-level_role_visible_fields).PaginatedResponse: Generic model for paginated API responses (seepydantic_visible_fields.paginatedresponse).
Functions
field(*, visible_to: Optional[List[Any]] = None, **kwargs) -> Any: Pydantic field replacement enablingvisible_to.configure_roles(*, role_enum: Type[Enum], inheritance: Optional[Dict[Any, Any]] = None, default_role: Optional[Union[Enum, str]] = None) -> None: Configures the global role system. Must be called once at startup.visible_fields_response(model: Any, role: Any = None) -> Any: Runtime helper to convert a model instance (or list/dict of instances) to its role-specific filtered response.
Methods (on VisibleFieldsMixin / VisibleFieldsModel instances/classes)
model_instance.visible_dict(role=None) -> Dict[str, Any]: Returns a dictionary containing only fields visible to the role (internal cycle marker{'__cycle_reference__': True}used).model_instance.to_response_model(role=None) -> BaseModel: Returns a validated Pydantic model instance containing only fields visible to the role (cycles replaced withNone). Usevisible_fields_responsehelper function for most use cases.ModelCls.create_response_model(role: str, model_name_suffix: str = "Response") -> Type[BaseModel]: Dynamically creates the Pydantic model type for a given role. Avoid calling at module level for complex models; prefer usingvisible_fields_responseat runtime.ModelCls.configure_visibility(role: Union[Enum, str], visible_fields: Set[str]) -> None: Dynamically updates visibility rules for a class (not thread-safe).
Development
Increasing version numbers
Use bump2version to increase the version number (included in the dev dependencies). E.g., bump2version patch or bump2version minor.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_visible_fields-0.2.6.tar.gz.
File metadata
- Download URL: pydantic_visible_fields-0.2.6.tar.gz
- Upload date:
- Size: 38.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bc1891d117f7ac0a8473291b47df353a7952dfc01bbaf7b6b6823427e715815
|
|
| MD5 |
7402da575f12d00f7a272e009ad23968
|
|
| BLAKE2b-256 |
e9d7aacef6006612e101b5ad447d2d2cc25fc0c0eb832a1eae16171653a5031a
|
File details
Details for the file pydantic_visible_fields-0.2.6-py3-none-any.whl.
File metadata
- Download URL: pydantic_visible_fields-0.2.6-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30fc954dd62b4f3b3a5728f56bf804269efc56a83186a77a5864ab3043c3e30a
|
|
| MD5 |
b4ac5a7854e21e20a96c662ad164aef9
|
|
| BLAKE2b-256 |
8ac238f5c666c3b87012bf09a46c12221c6826b71e264e72ec6e0339d4fbc3f1
|