Search code examples
pythonpython-3.xpydantic

Pydantic v2 custom type validators with info


I'm trying to update my code to pydantic v2 and having trouble finding a good way to replicate the custom types I had in version 1. I'll use my custom date type as an example. The original implementation and usage looked something like this:

from datetime import date
from pydantic import BaseModel


class CustomDate(date):
    # Override POTENTIAL_FORMATS and fill it with date format strings to match your data
    POTENTIAL_FORMATS = []
    
    @classmethod
    def __get_validators__(cls):
        yield cls.validate_date
        
    @classmethod
    def validate_date(cls, field_value, values, field, config) -> date:
        if type(field_value) is date:
            return field_value
        return to_date(field.name, field_value, cls.POTENTIAL_FORMATS, return_str=False)

class ExampleModel(BaseModel):
    class MyDate(CustomDate):
        POTENTIAL_FORMATS = ['%Y-%m-%d', '%Y/%m/%d']
    dt: MyDate

I tried to follow the official docs and the examples laid out here below and it mostly worked, but the info parameter does not have the fields I need (data and field_name). Attempting to access them gives me an AttributeError.

info.field_name
*** AttributeError: No attribute named 'field_name'

Both the Annotated and __get_pydantic_core_schema__ approaches have this issue

from datetime import date
from typing import Annotated

from pydantic import BaseModel, BeforeValidator
from pydantic_core import core_schema  

class CustomDate:
    POTENTIAL_FORMATS = []

    @classmethod
    def validate(cls, field_value, info):
        if type(field_value) is date:
            return field_value
        return to_date(info.field_name, field_value, potential_formats, return_str=False)

    @classmethod
    def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
        return core_schema.general_plain_validator_function(cls.validate)


def custom_date(potential_formats):
    """
    :param potential_formats: A list of datetime format strings
    """
    def validate_date(field_value, info) -> date:
        if type(field_value) is date:
            return field_value
        return to_date(info.field_name, field_value, potential_formats, return_str=False)
    CustomDate = Annotated[date, BeforeValidator(validate_date)]
    return CustomDate


class ExampleModel(BaseModel):
    class MyDate(CustomDate):
        POTENTIAL_FORMATS = ['%Y-%m-%d', '%Y/%m/%d']
    dt: MyDate
    dt2: custom_date(['%Y-%m-%d', '%Y/%m/%d'])

If I just include the validate_date function as a regular field_validator I get info with all the fields I need, it's only when using it with custom types that I see this issue. How do I write a custom type that has access to previously validated fields and the name of the field being validated?


Solution

  • As of version 2.4 you can get the field_name and data together. See the updated docs here.

    Now the first version of my custom data type looks like:

    class CustomDate:
        POTENTIAL_FORMATS = []
    
        @classmethod
        def validate(cls, field_value, info):
            if type(field_value) is date:
                return field_value
            return to_date(info.field_name, field_value, cls.POTENTIAL_FORMATS, return_str=False)
    
        @classmethod
        def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
            return core_schema.with_info_before_validator_function(
                cls.validate, handler(date), field_name=handler.field_name
            )
    

    Where all I needed to change was which core_schema validator function I was using. The second version of my custom data type (the one using Annotated) now works as is with no changes.

    Before Pydantic 2.4

    It looks like accessing info.data and info.field_name inside a custom type validator is not currently possible in v2 according to this feature request.

    If all you need is info.data, then it looks like you can define your validator with core_schema.field_before_validator_function (I'd guess all the field_* validators work), although you will need to make up a field name:

    from dataclasses import dataclass
    from typing import Annotated, List, Any, Callable
    
    
    from pydantic import ValidationError, BaseModel, Field, BeforeValidator, field_validator, GetCoreSchemaHandler
    from pydantic_core import core_schema, CoreSchema
    
    
    def fn(v: str, info: core_schema.ValidationInfo, *args, **kwargs) -> str:
        try:
            print(f'Validating {info.field_name}')
            return info.data['use_this']
        except AttributeError as err:
            return 'No data'
    
    
    class AsFieldB4Method(str):
        @classmethod
        def __get_pydantic_core_schema__(
            cls, source_type: Any, handler: GetCoreSchemaHandler, *args, **kwargs
        ) -> CoreSchema:
            return core_schema.field_before_validator_function(fn, 'not_the_real_field_name', core_schema.str_schema())
    
    
    class MyModel(BaseModel):
        use_this: str
        core_schema_field_b4_method: AsFieldB4Method  # Partially works
    

    From the comments, it sounds like the pydantic team want to make it work with non-field validators and to make accessing info.field_name possible, so hopefully that happens. I'll update this answer when the change happens, but check that link in case I missed it.