🛡️ Architecture Overview — pico-pydantic¶
pico-pydantic is an integration layer that enforces Runtime Type Safety within the Pico-IoC container using Pydantic models.
It implements an AOP (Aspect-Oriented Programming) pattern to:
- Intercept method calls to IoC components.
- Validate arguments against Pydantic schemas before the method executes.
- Decouple validation logic from business logic.
Its purpose is to turn Python type hints into enforced contracts.
1. High-Level Design¶
The library acts as a Guard Layer sitting between the caller and your service components.
┌───────────────────────────┐
│ Caller / Client │
│ (Controller, Test, etc.) │
└─────────────┬─────────────┘
│
(Calls Component)
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Pico-IoC Container │
│ │
│ ┌───────────────────────────┐ ┌───────────────────────────┐ │
│ │ ValidationInterceptor │ │ Target Service │ │
│ │ (Singleton) │ │ (Singleton/Proto) │ │
│ └─────────────┬─────────────┘ └─────────────┬─────────────┘ │
│ │ │ │
│ (Intercepts & Inspects) (Executes Logic) │
│ │ │ │
│ ┌────────▼────────┐ │ │
│ │ Pydantic │◄─────────────────────────┘ │
│ │ (Schema Check) │ (Only if Valid) │
│ └─────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
2. Architectural Comparison¶
This section contrasts the standard Python approach with the pico-pydantic design.
| Aspect | Standard Python / Manual | pico-pydantic Architecture |
|---|---|---|
| Type Safety | Advisory. Type hints are documentation. You can pass a str to an int argument, and Python won't complain until it crashes inside the method. | Enforced. Type hints are contracts. If you pass the wrong type or invalid data, execution is blocked before entering the method. |
| Validation Logic | Coupled & Repetitive. Methods are cluttered with manual if checks, isinstance calls, or explicit Model.model_validate(data) calls. | Transparent & Decoupled. Validation is handled by an Interceptor. The service code remains pure business logic, unaware of the validation mechanism. |
| Error Handling | Inconsistent. Different methods might raise ValueError, TypeError, or custom exceptions depending on how the developer wrote the check. | Standardized. All validation failures raise a uniform ValidationFailedError, wrapping the underlying Pydantic ValidationError. |
| Configuration | Imperative. You write validation code inside the function body. | Declarative. You simply decorate the method with @validate and use standard type hints. |
| Performance | Variable. Depends on the efficiency of manual checks. | Optimized. The interceptor uses metadata marking (VALIDATE_META) to skip non-validated methods instantly, minimizing overhead. |
3. Execution Flow¶
This flow describes what happens when a method decorated with @validate is called.
Method Call (args, kwargs)
│
▼
┌──────────────────────────────────────────────┐
│ [ValidationInterceptor] │
│ 1. Intercepts the call via pico-ioc hook │
│ 2. Checks for `@validate` marker │
│ 3. REFLECTION: Inspects function signature │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ [Argument Binding Strategy] │
│ 1. Detects `self` (Instance vs Class method) │
│ 2. Maps `args` & `kwargs` to param names │
│ 3. Applies default values │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ [Pydantic Bridge] │
│ FOR EACH parameter IN signature: │
│ IF type\_hint is Pydantic Model: │
│ try: │
│ TypeAdapter.validate\_python(value) │
│ except ValidationError: │
│ RAISE ValidationFailedError │
└──────────────────────┬───────────────────────┘
│ (All Valid)
▼
┌──────────────────────────────────────────────┐
│ [Target Component] │
│ Executes original method logic │
│ return result │
└──────────────────────────────────────────────┘
Validation Interceptor Flow (Mermaid)¶
flowchart TD
A["Method Call (args, kwargs)"] --> B{"Has @validate marker?"}
B -- No --> C["call_next(ctx) -- skip validation"]
B -- Yes --> D["inspect.signature(func)"]
D --> E["_bind_arguments(sig, args, kwargs)"]
E --> F["apply_defaults()"]
F --> G["Iterate parameters"]
G --> H{"_should_skip_param?\n(self/cls/no annotation)"}
H -- Yes --> G
H -- No --> I{"_requires_pydantic_validation?\n(BaseModel or generic with BaseModel)"}
I -- No --> G
I -- Yes --> J["TypeAdapter(annotation).validate_python(value)"]
J -- Success --> K["Replace argument with validated value"]
K --> G
J -- ValidationError --> L["Raise ValidationFailedError\n(wraps pydantic_error + method_name)"]
G -- All params done --> M["Update ctx.args and ctx.kwargs"]
M --> N["call_next(ctx) -- execute method"]
N --> O{"Result is awaitable?"}
O -- Yes --> P["await result"]
O -- No --> Q["return result"]
P --> Q
C --> O Type Resolution Tree (Mermaid)¶
flowchart TD
A["annotation"] --> B{"inspect.isclass(annotation)\nand issubclass(annotation, BaseModel)?"}
B -- Yes --> C["Return True -- requires validation"]
B -- No / Exception --> D{"Has __args__ attribute?"}
D -- No --> E["Return False -- no validation needed"]
D -- Yes --> F["Iterate annotation.__args__"]
F --> G["arg"]
G --> H{"_is_basemodel_class(arg)?"}
H -- Yes --> C
H -- No --> I{"_has_pydantic_in_args(arg, check_func)?"}
I -- Yes --> C
I -- No --> J{"More args?"}
J -- Yes --> G
J -- No --> E
style C fill:#4a4,color:#fff
style E fill:#a44,color:#fff Examples of type resolution:
| Annotation | Resolution Path |
|---|---|
UserModel | isclass + issubclass(BaseModel) -- True |
List[UserModel] | Not a class -- check __args__ -- (UserModel,) -- True |
Optional[UserModel] | Union[UserModel, None] -- __args__ -- (UserModel, None) -- True |
Union[str, int] | __args__ -- (str, int) -- neither is BaseModel -- False |
str | Not BaseModel, no __args__ -- False |
4. The Decorator Model (@validate)¶
The @validate decorator is lightweight. It does not contain validation logic. It serves as a Marker.
- Role: Metadata Tagging.
- Behavior: Sets a hidden attribute
_pico_pydantic_validate_meta = Trueon the function. - Reasoning: Keeping the decorator logic-free ensures that import times remain fast and allows the Interceptor (which has access to the full IoC context) to handle the heavy lifting.
from pico_pydantic import validate
class UserService:
# The decorator just says: "Hey Interceptor, look at me!"
@validate
def create_user(self, user: UserSchema):
# By the time we get here, 'user' is GUARANTEED
# to be a valid UserSchema instance.
pass
5. The Interceptor Model¶
The ValidationInterceptor is a MethodInterceptor component managed by Pico-IoC.
- Scope:
singleton(Created once, handles all calls). - Reflection: It uses
inspect.signatureto understand what arguments the method expects. - Heuristics: It includes logic to handle the discrepancy between how
pico-iocpasses arguments (whereselfmight be explicit) and how Python binds them.
Key Logic: It specifically looks for subclasses of pydantic.BaseModel. Standard types (int, str) are currently passed through without Pydantic validation to avoid excessive overhead, assuming Python's runtime handles basic primitives efficiently enough or Pydantic models are the primary concern for complex data.
6. Architectural Intent¶
pico-pydantic exists to:
- Promote Defensive Programming at the boundaries of your components.
- Eliminate "Boilerplate Validation Code" from your services.
- Ensure that if a Service receives a Data Transfer Object (DTO), it is valid, complete, and safe.
It does not attempt to:
- Replace Pydantic.
- Validate return values (currently focused on Inputs).
- Validate standard primitives (like
intranges) outside of a Pydantic Model context.
7. When to Use¶
Use pico-pydantic if your application needs:
✔ Strict Contracts: You want to guarantee that a User object passed to UserService is valid. ✔ Clean Services: You want to remove .model_validate() or try/except ValidationError blocks from your business logic. ✔ Fail-Fast Behavior: You want execution to stop before the method body is entered if data is invalid. ✔ IoC Integration: You are already using pico-ioc and want validation to feel native to the container.
Avoid pico-pydantic if:
✖ You are validating simple primitives (e.g., just an int > 0) without a Pydantic model (use standard assert or simple checks). ✖ You need extremely high-performance loops (reflection adds a small overhead per call). ✖ You are not using Pydantic models for your data structures.
8. Summary¶
pico-pydantic is a Quality Assurance layer:
It allows you to trust your method arguments.
The Decorator marks the contract. The Interceptor enforces the contract. The Service executes the contract.