Search code examples
pythonpydanticpydantic-v2

Pydantic - Migrate from v1 to v2 - @validator to @field_validator


As pydantic got upgraded from v1 to v2, I need to migrate the following piece of code from @validator to @field_validator. I'm facing issues to understand how can I validate more than one field.

  1. how can always behaviour be replicated in v2. In v2 this is not supported. Found validate_default=True in documentation but couldn't find a proper example to showcase the use.
  2. how to access field.name keyword. This is used to access all the arguments present in @validator decorator.

I understand that for values we can use info: ValidationInfo.

Does anyone know how could the piece of code be migrated to v2.

Thanks

class Task(BaseModel):
    name: Optional[str]
    pwd: Optional[str]
    depends_on: Optional[Union[str, List[str]]] = []
    features: Optional[Dict] = {}

    @validator("name", "pwd", "depends_on", "features", always=True)
        def validate_required_fields(cls, value, values, field):
            task_type = values.get("task_type")
            for task, fields in task_required.items():
                if task_type == task and field.name in fields:
                    if not value:
                        raise ValueError(f"{field} is mandatory for task type '{task_type}'")
            return value

Solution

  • The validate_default is an option in Field (docs). As you correctly identified, ValidationInfo (docs) now has all the information about the fields and the already validated data.

    A v2 validator could look like the following. Although you do not specify where task_type comes from (why is it not a permanent field?) and how task_required is defined.

    from typing import Any, Literal
    from pydantic import (
        BaseModel,
        field_validator,
        ValidationInfo,
        Field,
        ConfigDict,
    )
    
    task_required: dict[str, list[str]] = {
        "basic": [],
        "extra": ["depends_on"],
    }
    
    
    class Task(BaseModel):
        task_type: Literal["basic", "extra"] = Field(
            ...,
            description="Type of task",
        )
        name: str | None = Field(
            ...,
            description="Name of the task",
        )
        pwd: str | None = Field(
            ...,
            description="Working directory of the task",
        )
        depends_on: str | list[str] | None = Field(
            default_factory=list,
            description="Task dependencies",
            validate_default=True,
        )
        features: dict | None = Field(
            default_factory=dict,
            description="Task features",
            validate_default=True,
        )
    
        model_config = ConfigDict(extra="allow")
    
        @field_validator("name", "pwd", "depends_on", "features")
        @classmethod
        def validate_required_fields(cls, value: Any, info: ValidationInfo):
            task_type = info.data.get("task_type", "")
    
            if task_type not in task_required:
                raise ValueError(f'Invalid task type "{task_type}".')
    
            required_fields = task_required[task_type]
            if info.field_name in required_fields and not value:
                raise ValueError(
                    f'Field "{info.field_name}" is required for task "{task_type}".'
                )
    
            return value
    
    
    Task(task_type="extra", name="example", pwd=".")
    
    #ValidationError: 1 validation error for Task
    #depends_on
    #  Value error, Field "depends_on" is required for task "extra". [type=value_error, input_value=[], input_type=list]
    #    For further information visit https://errors.pydantic.dev/2.6/v/value_error
    
    

    It seems that you actually have to define task_type in the model. Otherwise, it was not available under info.data.