You can raise PydanticCustomError
from pydantic_core
, instead of ValueError
.
Your Pydantic Model will be something like this:
from datetime import date
from typing import Optional
from pydantic import (
BaseModel,
field_validator,
HttpUrl,
EmailStr
)
from pydantic import ValidationError
from pydantic_core import PydanticCustomError
class Company(BaseModel):
company_id: Optional[int] = None
company_name: Optional[str]
address: Optional[str]
state: Optional[str]
country: Optional[str]
postal_code: Optional[str]
phone_number: Optional[str]
email: Optional[EmailStr] = None
website_url: Optional[HttpUrl] = None
cin: Optional[str]
gst_in: Optional[str] = None
incorporation_date: Optional[date]
reporting_currency: Optional[str]
fy_start_date: Optional[date]
logo: Optional[str] = None
@field_validator('company_name')
def validate_company_name(cls, v):
if v is None or not v.strip():
raise PydanticCustomError(
'value_error', # This will be the "type" field
'Company name must be provided.', # This will be the "msg" field
)
return v
If you want a more sophisticated solution, you can view more about this discussion on Pydantic Repository. But basically you can create a WrapperClass to use with Annoted
type from typing
module.
I am gonne give my example because have also the ValidationInfo
parameter in the validation field method
import inspect
from pydantic import (
ValidationInfo,
ValidatorFunctionWrapHandler,
WrapValidator,
)
from pydantic_core import PydanticCustomError
class APIFriendlyErrorMessages:
"""
A WrapValidator that catches ValueError and AssertionError exceptions and
raises a PydanticCustomError with the message from the original exception,
while removing the error type prefix, which is not user-friendly.
"""
def __new__(cls, validator: Callable[[Any], None]) -> WrapValidator:
"""
Wrap a validator function with a WrapValidator that catches ValueError and
AssertionError exceptions and raises a PydanticCustomError with the message
from the original exception, while removing the error type prefix, which is
not user-friendly.
:param validator: The validator function to wrap.
:returns: A WrapValidator instance that prettifies error messages.
"""
# I added this, in the discussion he used just with "v" value
signature = inspect.signature(validator)
# Verify if the validate function has validation info parameter
has_validation_info = any(
param.annotation == ValidationInfo
for _, param in signature.parameters.items()
)
def _validator(
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
):
try:
# If not have validation info, call just with v
if not has_validation_info:
validator(v)
else:
# Or Else call with v and info
validator(v, info)
except ValueError as exc:
# This is the same Pydantic Custom Error we used before
raise PydanticCustomError(
'value_error',
str(exc),
)
return handler(v)
return WrapValidator(_validator)
And in my model:
from datetime import datetime
from decimal import Decimal
from typing import Annotated, Optional
from pydantic import BaseModel, Field, ValidationInfo, field_validator
from app.api.transactions.enums import PaymentMethod, TransactionType
from app.utils.schemas import APIFriendlyErrorMessages # Importing my Custom Wrapper
# Validate Function
def validate_total_installments(value: int, info: ValidationInfo) -> int:
if value > 1 and info.data['method'] != PaymentMethod.CREDIT_CARD:
# Raising ValueError
raise ValueError('Pagamentos a vista não podem ser parcelados.')
return value
# Annoted Type using the Wrapper and the validate Function
TotalInstallments = Annotated[int, APIFriendlyErrorMessages(validate_total_installments)]
class TransactionIn(BaseModel):
total: Decimal = Field(ge=Decimal('0.01'))
description: Optional[str] = None
type: TransactionType
method: PaymentMethod
total_installments: TotalInstallments = Field(ge=1, default=1) # Using your annoted type here
executed_at: datetime
bank_account_id: int
category_id: Optional[int] = None
I expect that help you.