1

I have a model for a FastAPI request and I want to add some validation for its fields.

from pydantic import BaseModel
from typing import List, Optional

class RequestModel(BaseModel):
    types: List[str]
    segments: Optional[List[str]] = []

For the above snippet I want to ensure that segments list should only contain values if the length of the types list is 1.

So, if the segments list has elements and length of types list is greater than 1 then it should return a validation error message to user.

How can this be done using Pydantic?

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
  • Have you seen the article in pydantics documentation about root validators? https://pydantic-docs.helpmanual.io/usage/validators/#root-validators – MatsLindh Nov 06 '22 at 17:50
  • @Chris I think you misunderstood the question. I don't see what value ranges have to do with this. Root validators would be a valid solution here. – Daniil Fajnberg Nov 06 '22 at 20:07
  • @Chris Fair enough. Although the documentation has a whole page on validators that explains this in a much more general way with multiple examples. I would assume OP has read that and is none the wiser, so I doubt a specific answer to a different problem would help. Certainly this question is not a duplicate. – Daniil Fajnberg Nov 06 '22 at 20:19
  • Related posts can also be found in [this answer](https://stackoverflow.com/a/71258131/17865804), as well as [this answer](https://stackoverflow.com/a/71228281/17865804) (see Update 2). – Chris Nov 06 '22 at 20:22

1 Answers1

0

You mean like this?

from typing import Any

from pydantic import BaseModel, ValidationError, root_validator


class RequestModel(BaseModel):
    types: list[str]
    segments: list[str] = []

    @root_validator
    def my_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
        if len(values.get("types", [])) > 1 and values["segments"]:
            raise ValueError("Your error message here")
        return values


if __name__ == "__main__":
    print(RequestModel(types=["foo"], segments=["a"]))
    print(RequestModel(types=["bar"]))
    print(RequestModel(types=[], segments=["b", "c"]))
    try:
        RequestModel(types=["spam", "eggs"], segments=["x"])
    except ValidationError as e:
        print(e)

Output:

types=['foo'] segments=['a']
types=['bar'] segments=[]
types=[] segments=['b', 'c']
1 validation error for RequestModel
__root__
  Your error message here (type=value_error)

An alternative is to use a normal @validator on the segments field and utilize the optional values parameter there. (see docs) This is how that could look:

from typing import Any

from pydantic import BaseModel, ValidationError, validator


class RequestModel(BaseModel):
    types: list[str]
    segments: list[str] = []

    @validator("segments")
    def my_validator(cls, v: list[str], values: dict[str, Any]) -> list[str]:
        if len(values.get("types", [])) > 1 and v:
            raise ValueError("Your error message here")
        return v


if __name__ == "__main__":
    print(RequestModel(types=["foo"], segments=["a"]))
    print(RequestModel(types=["bar"]))
    print(RequestModel(types=[], segments=["b", "c"]))
    try:
        RequestModel(types=["spam", "eggs"], segments=["x"])
    except ValidationError as e:
        print(e)

Output is equivalent with a slightly different error structure.

Here the order of the fields matters because values will only contain those that were already validated and validation is done in the order that fields are defined. So you can not do this with a @validator("types") because by the time that validator is called, values will not contain an entry for segments yet.


PS: The reason I used len(values.get("types", [])) instead of len(values["types"]) in both cases is to ensure a properly formed error message. types is a required field and a validation error will therefore occur anyway, if the user tries to initialize an instance without providing a value for it. But Pydantic models will still go through the entire validation cycle anyway, and collect all occurrences of ValueError, TypeError, and AssertionError, before giving out a comprehensive error message about all of them.

If we simply tried to get values["types"], but that key was not present in the dictionary because the user failed to provide it, this would trigger a KeyError, which is not properly processed as a ValidationError by Pydantic.

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41