- Валидация IPv4Address и IPv6Address под капотом в pydantic сделана немного косовато и отдельно от остальных типов
- Провалидировать адрес за один проход можно с помощью ip_address()
- Для формирования адекватного пути ошибки в составных типах можно использовать InitErrorDetails и указания относительного loc, pydantic умный и сформирует корректный абсолютный путь ошибки
Кастомная валидация IPv4Address и IPv6Address в Pydantic
Имеем на входе следующую модель:
from ipaddress import IPv4Address, IPv6Address, ip_address
from pydantic import BaseModel, ValidationError, field_validator
from pydantic_core import PydanticCustomError, InitErrorDetails
class Model(BaseModel):
first_ip: IPv4Address | IPv6Address
other_ips: list[IPv4Address | IPv6Address]
title: str
При невалидных данных получаем абсолютно не читаемые сообщения об ошибках:
Model(first_ip='127.0.259.1', other_ips=['127.258.0.1', '2001:0db8:85a3:0000:0000:8x2e:0370:7334'], title='some title')
# pydantic_core._pydantic_core.ValidationError: 6 validation errors for Model
# first_ip.lax-or-strict[lax=function-plain[ip_v4_address_validator()],strict=json-or-python[json=function-after[IPv4Address(), str],python=is-instance[IPv4Address]]]
# Input is not a valid IPv4 address [type=ip_v4_address, input_value='127.0.259.1', input_type=str]
# first_ip.lax-or-strict[lax=function-plain[ip_v6_address_validator()],strict=json-or-python[json=function-after[IPv6Address(), str],python=is-instance[IPv6Address]]]
# Input is not a valid IPv6 address [type=ip_v6_address, input_value='127.0.259.1', input_type=str]
# other_ips.0.lax-or-strict[lax=function-plain[ip_v4_address_validator()],strict=json-or-python[json=function-after[IPv4Address(), str],python=is-instance[IPv4Address]]]
# Input is not a valid IPv4 address [type=ip_v4_address, input_value='127.258.0.1', input_type=str]
# other_ips.0.lax-or-strict[lax=function-plain[ip_v6_address_validator()],strict=json-or-python[json=function-after[IPv6Address(), str],python=is-instance[IPv6Address]]]
# Input is not a valid IPv6 address [type=ip_v6_address, input_value='127.258.0.1', input_type=str]
# other_ips.1.lax-or-strict[lax=function-plain[ip_v4_address_validator()],strict=json-or-python[json=function-after[IPv4Address(), str],python=is-instance[IPv4Address]]]
# Input is not a valid IPv4 address [type=ip_v4_address, input_value='2001:0db8:85a3:0000:0000:8x2e:0370:7334', input_type=str]
# other_ips.1.lax-or-strict[lax=function-plain[ip_v6_address_validator()],strict=json-or-python[json=function-after[IPv6Address(), str],python=is-instance[IPv6Address]]]
# Input is not a valid IPv6 address [type=ip_v6_address, input_value='2001:0db8:85a3:0000:0000:8x2e:0370:7334', input_type=str]
Как видим, на невалидный адрес получаем по два сообщения с ошибкой и куча перегруженной информации. Можно избавиться от этого с помощью кастомного валидатора, к примеру:
def validate_ipaddress(raw_ipaddress: int | str | None) -> int | str | None:
if raw_ipaddress is not None:
try:
ip_address(raw_ipaddress)
except ValueError as exc:
raise PydanticCustomError('ip_address', 'Input is not a valid IPv4 address or IPv6 address') from exc
return raw_ipaddress
def validate_ipaddresses(raw_ipaddresses: list[int | str]) -> list[int | str]:
errors: list[InitErrorDetails] = []
# https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.InitErrorDetails
for index, raw_ipaddress in enumerate(raw_ipaddresses):
try:
validate_ipaddress(raw_ipaddress)
except PydanticCustomError as ip_address_exc:
errors.append(
InitErrorDetails(
type=ip_address_exc,
input=raw_ipaddress,
loc=(index,),
),
)
# https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.ValidationError.from_exception_data
if errors:
raise ValidationError.from_exception_data(title='ip_address_error', line_errors=errors)
return raw_ipaddresses
class Model(BaseModel):
first_ip: IPv4Address | IPv6Address
other_ips: list[IPv4Address | IPv6Address]
title: str
_validate_ipaddress = field_validator('first_ip', mode='before')(validate_ipaddress)
_validate_ipaddresses = field_validator('other_ips', mode='before')(validate_ipaddresses)
Смотрим как изменились сообщения об ошибках:
# pydantic_core._pydantic_core.ValidationError: 3 validation errors for Model
# first_ip
# Input is not a valid IPv4 address or IPv6 address [type=ip_address, input_value='127.0.259.1', input_type=str]
# other_ips.0
# Input is not a valid IPv4 address or IPv6 address [type=ip_address, input_value='127.258.0.1', input_type=str]
# other_ips.1
# Input is not a valid IPv4 address or IPv6 address [type=ip_address, input_value='2001:0db8:85a3:0000:0000:8x2e:0370:7334', input_type=str]