Pydantic validators don't raise validation errors immediately

Kelvin Wangonya - Mar 6 - - Dev Community

This one really had me confused. I was facing an error with Pydantic validators which I thought I had handled, but it just didn't work as expected.

The schema looked something like this:

class Schema(BaseModel):
    dates: List[str]
    start_date: Optional[str]
    end_date: Optional[str]
    ...

    @validator("dates")
    def update_date_format(cls, v):
        ...

    @validator("start_date")
    def set_start_date(cls, v, values):
        # some logic here that expects `dates` to exist
        v = values['dates'][0]
        return v

    @validator("end_date")
    def set_end_date(cls, v, values):
        # some logic here that expects `dates` to exist
        v = values['dates'][-1]
        return v
Enter fullscreen mode Exit fullscreen mode

My assumption in the last two validators was that dates would always be available because it's a required field.
To my surprise, an IndexError was always raised on set_start_date.

So I thought, ok, I'll raise a ValueError myself on update_date_format since the required field check doesn't seem to be working.

class Schema(BaseModel):
    dates: List[str]
    start_date: Optional[str]
    end_date: Optional[str]
    ...

    @validator("dates")
    def update_date_format(cls, v):
        if not v:
            raise ValueError("dates is a required field")
        ...

    @validator("start_date")
    def set_start_date(cls, v, values):
        # some logic here that expects `dates` to exist
        v = values['dates'][0]
        return v

    @validator("end_date")
    def set_end_date(cls, v, values):
        # some logic here that expects `dates` to exist
        v = values['dates'][-1]
        return v
Enter fullscreen mode Exit fullscreen mode

Again, an IndexError was raised on set_start_date. It's like my check on update_date_format was completely ignored.
I even put a breakpoint on the line to make sure it was being hit. It was.

And then it hit me. Validation errrors aren't raised immediately. They're collected and returned all at once in the response.

Despite seeing this countless times in 422 responses, I hadn't ever thought about it. It made perfect sense.
You don't want to return one error at a time as an API response. It's much better to return a response detailing everything that's wrong with the payload.

From the docs:

One exception will be raised regardless of the number of errors found, that ValidationError will contain information about all the errors and how they happened.

The important thing to remember is that this only happens for validation errors (i.e errors raised through ValueError or AssertionError).
This is why the IndexError was always raised immediately it happened.

How to stop validation on the first error

According to this answer:

If you have checks, the failure of which should interrupt the further validation, then put them in the pre=True root validator. Because field validation will not occur if pre=True root validators raise an error.

For example:

class PayloadValidator(BaseModel):
   emailId: List[str]
   role: str

   @root_validator(pre=True)
   def root_validate(cls, values):
       if not values['emailId']:
           raise ValueError("Email list is empty.")
       return values

   @validator("emailId")
   def valid_domains(cls, emailId):
       return emailId
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .