Validation adapters
Pydantic
Validate operation results against a Pydantic v2 model with the optional relprim.pydantic adapter. Validation without conversion, strict mode and failure reasons.
pydantic_validator turns a Pydantic v2 model into a RelPrim
validator. It works wherever a validator does:
validation_policy(...), @resilient(validation=...) and
.with_validation(...).
Install
pip install "relprim[pydantic]"
Pydantic v2 is supported (>=2.0.0,<3.0.0). Newer Python versions may need a
newer Pydantic release than 2.0. import relprim never imports Pydantic; only
relprim.pydantic does, and importing that module without the extra raises
ModuleNotFoundError with the pip install command.
Usage
from pydantic import BaseModel
from relprim import resilient, validation_policy
from relprim.pydantic import pydantic_validator
class User(BaseModel):
id: int
name: str
@resilient(retries=2, timeout=10, validation=validation_policy(pydantic_validator(User)))
async def fetch_user(user_id: int) -> dict[str, object]:
return await user_service.fetch(user_id)
pydantic_validator(model, *, name=None, strict=None) returns a
PydanticValidator. The name defaults to the model class name, here "User",
and must not be blank. Only BaseModel subclasses are supported.
Validation, not conversion
Values are checked with User.model_validate(value), and the resulting model
instance is discarded. The operation result is returned unchanged:
result = await fetch_user(42)
result.value # still the original dict, not a User instance
Pydantic’s normal coercion rules apply while validating. With the model above,
{"id": "1", "name": "Ada"} passes, and result.value still contains the string
"1". If you need the parsed model, call User.model_validate(result.value) in
your own code.
Strict mode
pydantic_validator(User, strict=True)
strict is forwarded to model_validate(...). True disables coercion, so
"1" no longer passes for an int field. The default, None, keeps the model’s
own configuration.
Validation failures
A value that does not match returns a failed ValidationResult, and RelPrim
raises ValidationFailedError for that attempt. The reason describes the first
Pydantic error with its location and error type, and counts the rest:
Field required (path: $.name, type: missing)
Input should be a valid integer (path: $.id, type: int_type)
Input should be a valid integer, unable to parse string as an integer (path: $.id, type: int_parsing) (+1 more validation error)
Paths start at $; fields are written as .name and list indexes as [1].
Errors are reported in Pydantic’s order, so the reason is deterministic. Input
values and documentation URLs from Pydantic errors are not included.
Retries and fallbacks
Model failures are ordinary ValidationFailedError failures, so the usual
validation rules apply: they are
retried when they match retry_on, and fallback results are validated with the
same policy, so a fallback can recover from a primary result the model rejected:
from relprim import ValidationFailedError, resilient, validation_policy
@resilient(
retries=2,
retry_on=(ValidationFailedError,),
fallback=fetch_user_from_replica,
validation=validation_policy(pydantic_validator(User)),
)
async def fetch_user(user_id: int) -> dict[str, object]:
return await user_service.fetch(user_id)
Limits
- Only Pydantic v2 models are supported.
- Operation results are never replaced with model instances.
- Only the first error is described in detail.
- Validation runs synchronously inside the attempt, like every validator.
