Skip to content
RelPrim docsPydantic

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.