Skip to content

API Reference

Complete API reference for Pico-Pydantic, auto-generated from source code docstrings.

Module Overview

Module Description
pico_pydantic Package exports and public API
pico_pydantic.decorators @validate decorator for Pydantic validation
pico_pydantic.interceptor Validation interceptor implementation

pico_pydantic

pico_pydantic

pico-pydantic: Declarative, AOP-based Pydantic validation for pico-ioc.

This package provides automatic argument validation for pico-ioc managed components using Pydantic BaseModel type hints. It intercepts method calls via AOP and validates arguments with TypeAdapter.validate_python() before the method body executes.

Public API

validate: Marker decorator that enables validation on a method. ValidationFailedError: Exception raised when argument validation fails. ValidationInterceptor: Singleton MethodInterceptor that performs validation.

Auto-discovery

Registered via the pico_boot.modules entry point. When using pico-boot, the ValidationInterceptor is auto-discovered and globally registered with the container.

Example

from pydantic import BaseModel, Field from pico_ioc import component from pico_pydantic import validate

class UserCreate(BaseModel): ... username: str = Field(min_length=3) ... email: str

@component ... class UserService: ... @validate ... async def create_user(self, data: UserCreate) -> dict: ... return data.model_dump()

ValidationFailedError

Bases: ValueError

Exception raised when Pydantic validation fails on method arguments.

Wraps the underlying Pydantic ValidationError with the name of the method that triggered the failure. Inherits from ValueError so it can be caught broadly as a value-related error.

Attributes:

Name Type Description
method_name

The name of the method whose arguments failed validation.

pydantic_error

The original Pydantic ValidationError instance.

Message format

"Validation failed for method '<method_name>': <pydantic_error>"

Example

from pico_pydantic import ValidationFailedError try: ... await service.create_user({"username": "ab"}) ... except ValidationFailedError as e: ... print(e.method_name) # "create_user" ... print(e.pydantic_error) # Original ValidationError

Source code in src/pico_pydantic/decorators.py
class ValidationFailedError(ValueError):
    """Exception raised when Pydantic validation fails on method arguments.

    Wraps the underlying Pydantic ``ValidationError`` with the name of the
    method that triggered the failure. Inherits from ``ValueError`` so it
    can be caught broadly as a value-related error.

    Attributes:
        method_name: The name of the method whose arguments failed validation.
        pydantic_error: The original Pydantic ``ValidationError`` instance.

    Message format:
        ``"Validation failed for method '<method_name>': <pydantic_error>"``

    Example:
        >>> from pico_pydantic import ValidationFailedError
        >>> try:
        ...     await service.create_user({"username": "ab"})
        ... except ValidationFailedError as e:
        ...     print(e.method_name)       # "create_user"
        ...     print(e.pydantic_error)    # Original ValidationError
    """

    def __init__(self, method_name: str, pydantic_error: Exception):
        """Initialize ValidationFailedError.

        Args:
            method_name: The name of the method that failed validation.
            pydantic_error: The original Pydantic ``ValidationError`` that
                describes which fields or constraints were violated.
        """
        self.method_name = method_name
        self.pydantic_error = pydantic_error
        super().__init__(f"Validation failed for method '{method_name}': {pydantic_error}")

__init__(method_name, pydantic_error)

Initialize ValidationFailedError.

Parameters:

Name Type Description Default
method_name str

The name of the method that failed validation.

required
pydantic_error Exception

The original Pydantic ValidationError that describes which fields or constraints were violated.

required
Source code in src/pico_pydantic/decorators.py
def __init__(self, method_name: str, pydantic_error: Exception):
    """Initialize ValidationFailedError.

    Args:
        method_name: The name of the method that failed validation.
        pydantic_error: The original Pydantic ``ValidationError`` that
            describes which fields or constraints were violated.
    """
    self.method_name = method_name
    self.pydantic_error = pydantic_error
    super().__init__(f"Validation failed for method '{method_name}': {pydantic_error}")

ValidationInterceptor

Bases: MethodInterceptor

AOP interceptor that validates method arguments against Pydantic schemas.

Registered as a singleton component via pico-ioc. When a method decorated with @validate is called, this interceptor:

  1. Checks for the @validate marker on the target method.
  2. Inspects the method signature for BaseModel type hints.
  3. Validates each matching argument using TypeAdapter(annotation).validate_python(value).
  4. Replaces dict arguments with validated BaseModel instances.
  5. Raises ValidationFailedError if any argument fails validation.

Only parameters with BaseModel type hints (or generic types containing BaseModel, such as List[Model], Optional[Model], Union[Model, ...]) are validated. Parameters without annotations or with non-Pydantic types (str, int, etc.) are passed through.

Auto-discovery

Registered via the pico_boot.modules entry point. No manual registration is needed when using pico-boot.

Example

from pydantic import BaseModel from pico_ioc import component from pico_pydantic import validate

class UserData(BaseModel): ... name: str ... age: int

@component ... class UserService: ... @validate ... async def create(self, data: UserData) -> dict: ... return data.model_dump()

Dicts are automatically converted to UserData instances:

await service.create({"name": "alice", "age": 30})

Source code in src/pico_pydantic/interceptor.py
@component(scope="singleton")
class ValidationInterceptor(MethodInterceptor):
    """AOP interceptor that validates method arguments against Pydantic schemas.

    Registered as a singleton component via pico-ioc. When a method
    decorated with ``@validate`` is called, this interceptor:

    1. Checks for the ``@validate`` marker on the target method.
    2. Inspects the method signature for ``BaseModel`` type hints.
    3. Validates each matching argument using
       ``TypeAdapter(annotation).validate_python(value)``.
    4. Replaces dict arguments with validated ``BaseModel`` instances.
    5. Raises ``ValidationFailedError`` if any argument fails validation.

    Only parameters with ``BaseModel`` type hints (or generic types
    containing ``BaseModel``, such as ``List[Model]``, ``Optional[Model]``,
    ``Union[Model, ...]``) are validated. Parameters without annotations
    or with non-Pydantic types (``str``, ``int``, etc.) are passed through.

    Auto-discovery:
        Registered via the ``pico_boot.modules`` entry point. No manual
        registration is needed when using ``pico-boot``.

    Example:
        >>> from pydantic import BaseModel
        >>> from pico_ioc import component
        >>> from pico_pydantic import validate
        >>>
        >>> class UserData(BaseModel):
        ...     name: str
        ...     age: int
        >>>
        >>> @component
        ... class UserService:
        ...     @validate
        ...     async def create(self, data: UserData) -> dict:
        ...         return data.model_dump()
        >>>
        >>> # Dicts are automatically converted to UserData instances:
        >>> # await service.create({"name": "alice", "age": 30})
    """

    async def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
        """Intercept a method call and validate arguments if marked.

        This is the main entry point called by pico-ioc's interceptor
        chain. If the target method has the ``@validate`` marker, its
        arguments are validated and transformed before proceeding.

        Args:
            ctx: The method invocation context provided by pico-ioc,
                containing the target class, method name, args, and kwargs.
            call_next: Callable to invoke the next interceptor or the
                actual method in the chain.

        Returns:
            The return value of the target method (or next interceptor).

        Raises:
            ValidationFailedError: If any argument with a ``BaseModel``
                type hint fails Pydantic validation. Wraps the original
                ``pydantic.ValidationError``.
        """
        original_func = getattr(ctx.cls, ctx.name, None)

        if not original_func or not getattr(original_func, VALIDATE_META, False):
            return await self._call_next_async(ctx, call_next)

        try:
            new_args, new_kwargs = self._validate_and_transform(original_func, ctx.args, ctx.kwargs)
            ctx.args = new_args
            ctx.kwargs = new_kwargs
        except ValidationError as e:
            raise ValidationFailedError(ctx.name, e) from e

        return await self._call_next_async(ctx, call_next)

    def _validate_and_transform(self, func: Callable, args: tuple, kwargs: dict) -> tuple[tuple, dict]:
        """Validate and transform method arguments using Pydantic.

        Binds the given positional and keyword arguments to the function
        signature, then iterates over each parameter. For parameters with
        ``BaseModel`` type hints (or generics containing them), the value
        is validated and potentially transformed (e.g., dicts become model
        instances) via ``TypeAdapter(annotation).validate_python(value)``.

        Args:
            func: The original method (used to obtain the signature).
            args: Positional arguments from the method call.
            kwargs: Keyword arguments from the method call.

        Returns:
            A tuple of ``(new_args, new_kwargs)`` with validated and
            transformed values.

        Raises:
            pydantic.ValidationError: If any argument fails validation.
                This is caught by ``invoke()`` and wrapped in
                ``ValidationFailedError``.
        """
        sig = inspect.signature(func)
        bound = _bind_arguments(sig, args, kwargs)
        bound.apply_defaults()

        validated_args_map = bound.arguments.copy()

        for name, val in bound.arguments.items():
            param = sig.parameters[name]

            if _should_skip_param(name, param.annotation):
                continue

            if self._requires_pydantic_validation(param.annotation):
                validated_args_map[name] = TypeAdapter(param.annotation).validate_python(val)

        bound.arguments.update(validated_args_map)
        return bound.args, bound.kwargs

    def _requires_pydantic_validation(self, annotation: Any) -> bool:
        """Determine whether a type annotation requires Pydantic validation.

        Checks if the annotation is a ``BaseModel`` subclass directly, or
        if it is a generic type (``List``, ``Optional``, ``Union``, etc.)
        whose ``__args__`` contain a ``BaseModel`` subclass. The check is
        recursive, so deeply nested generics are supported.

        Any exception during the check (e.g., ``TypeError`` from
        ``issubclass`` on special typing constructs, or broken
        ``__args__`` iterators) is silently caught, and ``False`` is
        returned.

        Args:
            annotation: The type annotation to inspect.

        Returns:
            ``True`` if the annotation involves a ``BaseModel`` type that
            should be validated, ``False`` otherwise.
        """
        try:
            if _is_basemodel_class(annotation):
                return True
            return _has_pydantic_in_args(annotation, self._requires_pydantic_validation)
        except Exception:
            return False

    async def _call_next_async(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
        """Invoke the next handler in the interceptor chain.

        Supports both synchronous and asynchronous method implementations.
        If ``call_next`` returns an awaitable (coroutine), it is awaited;
        otherwise the result is returned directly.

        Args:
            ctx: The method invocation context.
            call_next: Callable to invoke the next interceptor or the
                actual method.

        Returns:
            The result of the next handler, whether sync or async.
        """
        res = call_next(ctx)
        if inspect.isawaitable(res):
            return await res
        return res

invoke(ctx, call_next) async

Intercept a method call and validate arguments if marked.

This is the main entry point called by pico-ioc's interceptor chain. If the target method has the @validate marker, its arguments are validated and transformed before proceeding.

Parameters:

Name Type Description Default
ctx MethodCtx

The method invocation context provided by pico-ioc, containing the target class, method name, args, and kwargs.

required
call_next Callable[[MethodCtx], Any]

Callable to invoke the next interceptor or the actual method in the chain.

required

Returns:

Type Description
Any

The return value of the target method (or next interceptor).

Raises:

Type Description
ValidationFailedError

If any argument with a BaseModel type hint fails Pydantic validation. Wraps the original pydantic.ValidationError.

Source code in src/pico_pydantic/interceptor.py
async def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
    """Intercept a method call and validate arguments if marked.

    This is the main entry point called by pico-ioc's interceptor
    chain. If the target method has the ``@validate`` marker, its
    arguments are validated and transformed before proceeding.

    Args:
        ctx: The method invocation context provided by pico-ioc,
            containing the target class, method name, args, and kwargs.
        call_next: Callable to invoke the next interceptor or the
            actual method in the chain.

    Returns:
        The return value of the target method (or next interceptor).

    Raises:
        ValidationFailedError: If any argument with a ``BaseModel``
            type hint fails Pydantic validation. Wraps the original
            ``pydantic.ValidationError``.
    """
    original_func = getattr(ctx.cls, ctx.name, None)

    if not original_func or not getattr(original_func, VALIDATE_META, False):
        return await self._call_next_async(ctx, call_next)

    try:
        new_args, new_kwargs = self._validate_and_transform(original_func, ctx.args, ctx.kwargs)
        ctx.args = new_args
        ctx.kwargs = new_kwargs
    except ValidationError as e:
        raise ValidationFailedError(ctx.name, e) from e

    return await self._call_next_async(ctx, call_next)

validate(func)

Marker decorator that enables Pydantic validation on a method.

Marks the decorated function with a hidden metadata attribute (_pico_pydantic_validate_meta) so the ValidationInterceptor knows to inspect and validate its arguments at call time.

This decorator does not contain validation logic itself. It is intentionally lightweight to keep import times fast and to delegate the heavy lifting to the interceptor, which has access to the full IoC context.

Parameters:

Name Type Description Default
func T

The function or method to mark for validation.

required

Returns:

Type Description
T

The same function, unmodified except for the added metadata attribute.

Example

from pico_ioc import component from pico_pydantic import validate from pydantic import BaseModel

class ItemData(BaseModel): ... name: str ... price: float

@component ... class ItemService: ... @validate ... async def add_item(self, data: ItemData) -> dict: ... return data.model_dump()

Source code in src/pico_pydantic/decorators.py
def validate(func: T) -> T:
    """Marker decorator that enables Pydantic validation on a method.

    Marks the decorated function with a hidden metadata attribute
    (``_pico_pydantic_validate_meta``) so the ``ValidationInterceptor``
    knows to inspect and validate its arguments at call time.

    This decorator does **not** contain validation logic itself. It is
    intentionally lightweight to keep import times fast and to delegate
    the heavy lifting to the interceptor, which has access to the full
    IoC context.

    Args:
        func: The function or method to mark for validation.

    Returns:
        The same function, unmodified except for the added metadata attribute.

    Example:
        >>> from pico_ioc import component
        >>> from pico_pydantic import validate
        >>> from pydantic import BaseModel
        >>>
        >>> class ItemData(BaseModel):
        ...     name: str
        ...     price: float
        >>>
        >>> @component
        ... class ItemService:
        ...     @validate
        ...     async def add_item(self, data: ItemData) -> dict:
        ...         return data.model_dump()
    """
    setattr(func, VALIDATE_META, True)
    return func

Decorators

pico_pydantic.decorators

Decorators and exceptions for pico-pydantic validation.

This module provides
  • The @validate marker decorator that flags methods for argument validation by the ValidationInterceptor.
  • The ValidationFailedError exception raised when Pydantic validation fails on a method argument.

VALIDATE_META = '_pico_pydantic_validate_meta' module-attribute

str: Hidden attribute name set on functions by @validate.

The ValidationInterceptor checks for this attribute to determine whether a method should undergo argument validation.

ValidationFailedError

Bases: ValueError

Exception raised when Pydantic validation fails on method arguments.

Wraps the underlying Pydantic ValidationError with the name of the method that triggered the failure. Inherits from ValueError so it can be caught broadly as a value-related error.

Attributes:

Name Type Description
method_name

The name of the method whose arguments failed validation.

pydantic_error

The original Pydantic ValidationError instance.

Message format

"Validation failed for method '<method_name>': <pydantic_error>"

Example

from pico_pydantic import ValidationFailedError try: ... await service.create_user({"username": "ab"}) ... except ValidationFailedError as e: ... print(e.method_name) # "create_user" ... print(e.pydantic_error) # Original ValidationError

Source code in src/pico_pydantic/decorators.py
class ValidationFailedError(ValueError):
    """Exception raised when Pydantic validation fails on method arguments.

    Wraps the underlying Pydantic ``ValidationError`` with the name of the
    method that triggered the failure. Inherits from ``ValueError`` so it
    can be caught broadly as a value-related error.

    Attributes:
        method_name: The name of the method whose arguments failed validation.
        pydantic_error: The original Pydantic ``ValidationError`` instance.

    Message format:
        ``"Validation failed for method '<method_name>': <pydantic_error>"``

    Example:
        >>> from pico_pydantic import ValidationFailedError
        >>> try:
        ...     await service.create_user({"username": "ab"})
        ... except ValidationFailedError as e:
        ...     print(e.method_name)       # "create_user"
        ...     print(e.pydantic_error)    # Original ValidationError
    """

    def __init__(self, method_name: str, pydantic_error: Exception):
        """Initialize ValidationFailedError.

        Args:
            method_name: The name of the method that failed validation.
            pydantic_error: The original Pydantic ``ValidationError`` that
                describes which fields or constraints were violated.
        """
        self.method_name = method_name
        self.pydantic_error = pydantic_error
        super().__init__(f"Validation failed for method '{method_name}': {pydantic_error}")

__init__(method_name, pydantic_error)

Initialize ValidationFailedError.

Parameters:

Name Type Description Default
method_name str

The name of the method that failed validation.

required
pydantic_error Exception

The original Pydantic ValidationError that describes which fields or constraints were violated.

required
Source code in src/pico_pydantic/decorators.py
def __init__(self, method_name: str, pydantic_error: Exception):
    """Initialize ValidationFailedError.

    Args:
        method_name: The name of the method that failed validation.
        pydantic_error: The original Pydantic ``ValidationError`` that
            describes which fields or constraints were violated.
    """
    self.method_name = method_name
    self.pydantic_error = pydantic_error
    super().__init__(f"Validation failed for method '{method_name}': {pydantic_error}")

validate(func)

Marker decorator that enables Pydantic validation on a method.

Marks the decorated function with a hidden metadata attribute (_pico_pydantic_validate_meta) so the ValidationInterceptor knows to inspect and validate its arguments at call time.

This decorator does not contain validation logic itself. It is intentionally lightweight to keep import times fast and to delegate the heavy lifting to the interceptor, which has access to the full IoC context.

Parameters:

Name Type Description Default
func T

The function or method to mark for validation.

required

Returns:

Type Description
T

The same function, unmodified except for the added metadata attribute.

Example

from pico_ioc import component from pico_pydantic import validate from pydantic import BaseModel

class ItemData(BaseModel): ... name: str ... price: float

@component ... class ItemService: ... @validate ... async def add_item(self, data: ItemData) -> dict: ... return data.model_dump()

Source code in src/pico_pydantic/decorators.py
def validate(func: T) -> T:
    """Marker decorator that enables Pydantic validation on a method.

    Marks the decorated function with a hidden metadata attribute
    (``_pico_pydantic_validate_meta``) so the ``ValidationInterceptor``
    knows to inspect and validate its arguments at call time.

    This decorator does **not** contain validation logic itself. It is
    intentionally lightweight to keep import times fast and to delegate
    the heavy lifting to the interceptor, which has access to the full
    IoC context.

    Args:
        func: The function or method to mark for validation.

    Returns:
        The same function, unmodified except for the added metadata attribute.

    Example:
        >>> from pico_ioc import component
        >>> from pico_pydantic import validate
        >>> from pydantic import BaseModel
        >>>
        >>> class ItemData(BaseModel):
        ...     name: str
        ...     price: float
        >>>
        >>> @component
        ... class ItemService:
        ...     @validate
        ...     async def add_item(self, data: ItemData) -> dict:
        ...         return data.model_dump()
    """
    setattr(func, VALIDATE_META, True)
    return func

Interceptor

pico_pydantic.interceptor

AOP validation interceptor for pico-ioc managed components.

This module implements the ValidationInterceptor, a singleton MethodInterceptor that inspects method signatures for Pydantic BaseModel type hints and validates arguments using TypeAdapter.validate_python() before the method body executes.

Helper functions are extracted at module level to keep cyclomatic complexity low: - _bind_arguments: Binds positional/keyword args to a signature. - _should_skip_param: Determines if a parameter should bypass validation. - _is_basemodel_class: Checks if an annotation is a BaseModel subclass. - _has_pydantic_in_args: Recursively checks generic __args__ for BaseModel types.

ValidationInterceptor

Bases: MethodInterceptor

AOP interceptor that validates method arguments against Pydantic schemas.

Registered as a singleton component via pico-ioc. When a method decorated with @validate is called, this interceptor:

  1. Checks for the @validate marker on the target method.
  2. Inspects the method signature for BaseModel type hints.
  3. Validates each matching argument using TypeAdapter(annotation).validate_python(value).
  4. Replaces dict arguments with validated BaseModel instances.
  5. Raises ValidationFailedError if any argument fails validation.

Only parameters with BaseModel type hints (or generic types containing BaseModel, such as List[Model], Optional[Model], Union[Model, ...]) are validated. Parameters without annotations or with non-Pydantic types (str, int, etc.) are passed through.

Auto-discovery

Registered via the pico_boot.modules entry point. No manual registration is needed when using pico-boot.

Example

from pydantic import BaseModel from pico_ioc import component from pico_pydantic import validate

class UserData(BaseModel): ... name: str ... age: int

@component ... class UserService: ... @validate ... async def create(self, data: UserData) -> dict: ... return data.model_dump()

Dicts are automatically converted to UserData instances:

await service.create({"name": "alice", "age": 30})

Source code in src/pico_pydantic/interceptor.py
@component(scope="singleton")
class ValidationInterceptor(MethodInterceptor):
    """AOP interceptor that validates method arguments against Pydantic schemas.

    Registered as a singleton component via pico-ioc. When a method
    decorated with ``@validate`` is called, this interceptor:

    1. Checks for the ``@validate`` marker on the target method.
    2. Inspects the method signature for ``BaseModel`` type hints.
    3. Validates each matching argument using
       ``TypeAdapter(annotation).validate_python(value)``.
    4. Replaces dict arguments with validated ``BaseModel`` instances.
    5. Raises ``ValidationFailedError`` if any argument fails validation.

    Only parameters with ``BaseModel`` type hints (or generic types
    containing ``BaseModel``, such as ``List[Model]``, ``Optional[Model]``,
    ``Union[Model, ...]``) are validated. Parameters without annotations
    or with non-Pydantic types (``str``, ``int``, etc.) are passed through.

    Auto-discovery:
        Registered via the ``pico_boot.modules`` entry point. No manual
        registration is needed when using ``pico-boot``.

    Example:
        >>> from pydantic import BaseModel
        >>> from pico_ioc import component
        >>> from pico_pydantic import validate
        >>>
        >>> class UserData(BaseModel):
        ...     name: str
        ...     age: int
        >>>
        >>> @component
        ... class UserService:
        ...     @validate
        ...     async def create(self, data: UserData) -> dict:
        ...         return data.model_dump()
        >>>
        >>> # Dicts are automatically converted to UserData instances:
        >>> # await service.create({"name": "alice", "age": 30})
    """

    async def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
        """Intercept a method call and validate arguments if marked.

        This is the main entry point called by pico-ioc's interceptor
        chain. If the target method has the ``@validate`` marker, its
        arguments are validated and transformed before proceeding.

        Args:
            ctx: The method invocation context provided by pico-ioc,
                containing the target class, method name, args, and kwargs.
            call_next: Callable to invoke the next interceptor or the
                actual method in the chain.

        Returns:
            The return value of the target method (or next interceptor).

        Raises:
            ValidationFailedError: If any argument with a ``BaseModel``
                type hint fails Pydantic validation. Wraps the original
                ``pydantic.ValidationError``.
        """
        original_func = getattr(ctx.cls, ctx.name, None)

        if not original_func or not getattr(original_func, VALIDATE_META, False):
            return await self._call_next_async(ctx, call_next)

        try:
            new_args, new_kwargs = self._validate_and_transform(original_func, ctx.args, ctx.kwargs)
            ctx.args = new_args
            ctx.kwargs = new_kwargs
        except ValidationError as e:
            raise ValidationFailedError(ctx.name, e) from e

        return await self._call_next_async(ctx, call_next)

    def _validate_and_transform(self, func: Callable, args: tuple, kwargs: dict) -> tuple[tuple, dict]:
        """Validate and transform method arguments using Pydantic.

        Binds the given positional and keyword arguments to the function
        signature, then iterates over each parameter. For parameters with
        ``BaseModel`` type hints (or generics containing them), the value
        is validated and potentially transformed (e.g., dicts become model
        instances) via ``TypeAdapter(annotation).validate_python(value)``.

        Args:
            func: The original method (used to obtain the signature).
            args: Positional arguments from the method call.
            kwargs: Keyword arguments from the method call.

        Returns:
            A tuple of ``(new_args, new_kwargs)`` with validated and
            transformed values.

        Raises:
            pydantic.ValidationError: If any argument fails validation.
                This is caught by ``invoke()`` and wrapped in
                ``ValidationFailedError``.
        """
        sig = inspect.signature(func)
        bound = _bind_arguments(sig, args, kwargs)
        bound.apply_defaults()

        validated_args_map = bound.arguments.copy()

        for name, val in bound.arguments.items():
            param = sig.parameters[name]

            if _should_skip_param(name, param.annotation):
                continue

            if self._requires_pydantic_validation(param.annotation):
                validated_args_map[name] = TypeAdapter(param.annotation).validate_python(val)

        bound.arguments.update(validated_args_map)
        return bound.args, bound.kwargs

    def _requires_pydantic_validation(self, annotation: Any) -> bool:
        """Determine whether a type annotation requires Pydantic validation.

        Checks if the annotation is a ``BaseModel`` subclass directly, or
        if it is a generic type (``List``, ``Optional``, ``Union``, etc.)
        whose ``__args__`` contain a ``BaseModel`` subclass. The check is
        recursive, so deeply nested generics are supported.

        Any exception during the check (e.g., ``TypeError`` from
        ``issubclass`` on special typing constructs, or broken
        ``__args__`` iterators) is silently caught, and ``False`` is
        returned.

        Args:
            annotation: The type annotation to inspect.

        Returns:
            ``True`` if the annotation involves a ``BaseModel`` type that
            should be validated, ``False`` otherwise.
        """
        try:
            if _is_basemodel_class(annotation):
                return True
            return _has_pydantic_in_args(annotation, self._requires_pydantic_validation)
        except Exception:
            return False

    async def _call_next_async(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
        """Invoke the next handler in the interceptor chain.

        Supports both synchronous and asynchronous method implementations.
        If ``call_next`` returns an awaitable (coroutine), it is awaited;
        otherwise the result is returned directly.

        Args:
            ctx: The method invocation context.
            call_next: Callable to invoke the next interceptor or the
                actual method.

        Returns:
            The result of the next handler, whether sync or async.
        """
        res = call_next(ctx)
        if inspect.isawaitable(res):
            return await res
        return res

invoke(ctx, call_next) async

Intercept a method call and validate arguments if marked.

This is the main entry point called by pico-ioc's interceptor chain. If the target method has the @validate marker, its arguments are validated and transformed before proceeding.

Parameters:

Name Type Description Default
ctx MethodCtx

The method invocation context provided by pico-ioc, containing the target class, method name, args, and kwargs.

required
call_next Callable[[MethodCtx], Any]

Callable to invoke the next interceptor or the actual method in the chain.

required

Returns:

Type Description
Any

The return value of the target method (or next interceptor).

Raises:

Type Description
ValidationFailedError

If any argument with a BaseModel type hint fails Pydantic validation. Wraps the original pydantic.ValidationError.

Source code in src/pico_pydantic/interceptor.py
async def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
    """Intercept a method call and validate arguments if marked.

    This is the main entry point called by pico-ioc's interceptor
    chain. If the target method has the ``@validate`` marker, its
    arguments are validated and transformed before proceeding.

    Args:
        ctx: The method invocation context provided by pico-ioc,
            containing the target class, method name, args, and kwargs.
        call_next: Callable to invoke the next interceptor or the
            actual method in the chain.

    Returns:
        The return value of the target method (or next interceptor).

    Raises:
        ValidationFailedError: If any argument with a ``BaseModel``
            type hint fails Pydantic validation. Wraps the original
            ``pydantic.ValidationError``.
    """
    original_func = getattr(ctx.cls, ctx.name, None)

    if not original_func or not getattr(original_func, VALIDATE_META, False):
        return await self._call_next_async(ctx, call_next)

    try:
        new_args, new_kwargs = self._validate_and_transform(original_func, ctx.args, ctx.kwargs)
        ctx.args = new_args
        ctx.kwargs = new_kwargs
    except ValidationError as e:
        raise ValidationFailedError(ctx.name, e) from e

    return await self._call_next_async(ctx, call_next)