Frequently Asked Questions¶
General¶
What is Pico-Pydantic?¶
Pico-Pydantic provides AOP-based argument validation for pico-ioc managed components. It uses Pydantic's TypeAdapter to validate method arguments against BaseModel type hints before the method executes.
What Python versions are supported?¶
Pico-Pydantic requires Python 3.11 or later.
What Pydantic version is required?¶
Pydantic 2.0 or later is required.
Validation¶
How does validation work?¶
The @validate decorator is a lightweight marker. The actual validation is performed by the ValidationInterceptor, a singleton MethodInterceptor that:
- Intercepts the method call
- Inspects the method signature for
BaseModeltype hints - Validates each argument using
TypeAdapter.validate_python() - Transforms dicts into model instances automatically
- Raises
ValidationFailedErrorif validation fails
Which types are validated?¶
Only arguments with Pydantic BaseModel type hints (or generics containing them):
@validate
async def process(self, user: UserModel, count: int):
# 'user' is validated, 'count' is passed through
...
Supported generic types: List[BaseModel], Optional[BaseModel], Union[BaseModel, ...].
Are dicts automatically converted to models?¶
Yes. If an argument has a BaseModel type hint and you pass a dict, it is validated and converted:
# Both calls work:
await service.create(UserModel(name="alice"))
await service.create({"name": "alice"}) # Dict converted to UserModel
How do I handle validation errors?¶
Catch ValidationFailedError:
from pico_pydantic import ValidationFailedError
try:
await service.create({"invalid": "data"})
except ValidationFailedError as e:
print(e.method_name) # Method that failed
print(e.pydantic_error) # Original Pydantic ValidationError
ValidationFailedError inherits from ValueError.
Architecture¶
Why use a decorator + interceptor instead of inline validation?¶
Separation of concerns. The @validate decorator is a marker that keeps your business logic clean. The ValidationInterceptor handles all validation logic via AOP, so services don't need model_validate() calls or try/except blocks.
Does it work with sync methods?¶
Yes. The interceptor supports both async def and regular methods.
Is there a performance overhead?¶
The interceptor checks for the @validate marker first. Methods without it are skipped instantly. For validated methods, a TypeAdapter is created per parameter per call. This is suitable for service-layer validation where correctness matters more than microsecond latency.
Troubleshooting¶
Validation is not running¶
The most common cause: the component was not resolved from the container.
@validate is a marker — it does nothing on its own. The actual validation is performed by the ValidationInterceptor, which is part of pico-ioc's AOP pipeline. This pipeline only runs for components obtained via container.get() (or injected by the container into another component).
# Validation runs — interceptor is active
service = container.get(MyService)
await service.process({"bad": "data"}) # -> ValidationFailedError
# Validation does NOT run — no interceptor
service = MyService()
await service.process({"bad": "data"}) # -> executes normally
If you are getting the service from the container and validation still does not run, check these additional causes:
-
Missing
@validateon the method: -
ValidationInterceptornot registered. If you usepico-boot, it is auto-discovered via thepico_boot.modulesentry point. If you do not usepico-boot, add"pico_pydantic"to your modules list: -
The parameter is not a
BaseModeltype. OnlyBaseModelsubclasses (and generics likeList[Model],Optional[Model]) are validated. Plain types (str,int) are passed through.
See the unified troubleshooting guide for a complete decision tree.
Arguments are not being validated¶
Only parameters with BaseModel type hints are validated. Parameters without annotations, or with non-Pydantic types, are passed through:
@validate
async def process(self, data: MyModel, name: str, count):
# data: validated (BaseModel)
# name: passed through (str, not BaseModel)
# count: passed through (no annotation)
...
Error Reference¶
This section documents every error message produced by pico-pydantic with its exact text, cause, and fix.
ValidationFailedError¶
Exact message format:
Example output:
Validation failed for method 'create_user': 1 validation error for UserCreate
username
String should have at least 3 characters [type=string_too_short, input_value='ab', input_type=str]
Cause: An argument with a BaseModel type hint failed Pydantic validation. The interceptor called TypeAdapter(annotation).validate_python(value) and Pydantic raised a ValidationError.
Attributes:
| Attribute | Type | Description |
|---|---|---|
method_name | str | Name of the method that failed validation |
pydantic_error | pydantic.ValidationError | The original Pydantic error with field details |
Fix: Inspect e.pydantic_error.errors() to see which fields failed and why:
from pico_pydantic import ValidationFailedError
try:
await service.create_user({"username": "ab"})
except ValidationFailedError as e:
for error in e.pydantic_error.errors():
print(f"Field: {error['loc']}, Message: {error['msg']}")
ValidationFailedError inherits from ValueError, so it can be caught with except ValueError as well.
TypeError from argument binding¶
Exact message format (Python standard):
or
Cause: The _bind_arguments() helper tries to bind positional and keyword arguments to the method signature. If binding fails even after adjusting for self/cls, Python raises a TypeError.
Fix: Ensure the arguments you pass match the method signature (correct number of positional args, correct keyword names).
TypeAdapter validation errors¶
Exact message format (from Pydantic):
N validation error(s) for <TypeName>
<field_name>
<error_message> [type=<error_type>, input_value=<value>, input_type=<type>]
Example:
2 validation errors for ItemData
name
String should have at least 3 characters [type=string_too_short, input_value='ab', input_type=str]
price
Input should be greater than 0 [type=greater_than, input_value=-5, input_type=int]
Cause: TypeAdapter(annotation).validate_python(value) found that the input data violates one or more Pydantic field constraints. This error is always wrapped in ValidationFailedError by the interceptor, so you will not see a bare pydantic.ValidationError unless you bypass the interceptor.
Fix: Correct the input data to satisfy the model's field constraints (Field(min_length=...), Field(gt=...), required fields, etc.).
Silent failure on broken generic types¶
Symptom: No ValidationFailedError is raised even though the type annotation looks like it should be validated.
Cause: The _requires_pydantic_validation() method wraps its type-checking logic in a broad except Exception handler. If a type annotation has a broken __args__ iterator or causes issubclass() to raise a TypeError (common with some typing constructs), the method returns False and the argument is silently passed through.
Fix: This is by design to avoid crashes on exotic type annotations. If you suspect a type is not being validated, verify that issubclass(YourType, BaseModel) works correctly in an interactive Python session. Standard BaseModel subclasses and common generics (List, Optional, Union) are fully supported.