Home > other >  Check if a field exists depending on some condition in another field
Check if a field exists depending on some condition in another field

Time:12-23

I am using Pydantic for JSON fields type validation.

I have this JSON:

json = {
    "id": "string",
    "type": "string enum[initial, calc]",
    "source": "string if type == initial",
    "source_path": "string if type == initial",
    "formula": "string if type == calc"
}

How can I use Pydantic to check the fields with an if-condition?

Or is Pydantic not right tool for it and I should use parsing with my own functions?

This code doesn't work:

from enum import Enum

from pydantic import BaseModel, validator


class Type(Enum):
    initial = "initial"
    calc = "calc"


class Tag(BaseModel):
    id: str
    type: Type

    @validator("type")
    def check_type(cls, field_value):
        if field_value == "calc":
            formula: str
        else:
            source: str
            source_path: str
# correct json
json_corr = {
    "id": "string",
    "type": "calc",
    "formula": "string"
}
Tag.parse_obj(json_corr)

# incorrect json
json_incorr = {
    "id": "string",
    "type": "calc",
    "source": "string",
    "source_path": "string",
}
# no error, but there should be
Tag.parse_obj(json_incorr)

CodePudding user response:

The definition of your Tag model seems to be incomplete. The way you describe your desired behavior, you should define three additional fields and make them optional (by defining a sensible default for them). Then you can perform the checks you want.

Also, inside your check_type method, you aren't actually assigning or checking anything; you are just creating type annotations for formula (or source/source_path) in the local namespace of that method. Those names and annotations do not magically carry over to the class namespace. Besides, a validator must always return a value to be assigned to the field it is specified for (in this case type). That or raise an appropriate exception.

validators should either return the parsed value or raise a ValueError, TypeError, or AssertionError

(see docs)

Finally, I would suggest using a root_validator for your purposes instead. They are designed specifically to perform checks on the entire model's data, which seems appropriate in your case.

Here is what I would suggest:

from enum import Enum

from pydantic import BaseModel, root_validator


class TypeEnum(Enum):
    initial = "initial"
    calc = "calc"


class Tag(BaseModel):
    id: str
    type: TypeEnum
    formula: str | None = None
    source: str | None = None
    source_path: str | None = None

    @root_validator
    def check_type(cls, values: dict[str, object]) -> dict[str, object]:
        if values["type"] == TypeEnum.calc and values.get("formula") is None:
            raise ValueError("`calc` requires `formula`")
        if values["type"] == TypeEnum.initial:
            if any(values[k] is None for k in {"source", "source_path"}):
                raise ValueError("`initial` requires `source` & `source_path`")
        return values

...

If you are on Python <3.10, use Optional[str] instead of str | None as the annotation for the optional fields.

Here is a quick test for it:

...

if __name__ == "__main__":
    from pydantic import ValidationError
    
    print(Tag.parse_obj({
        "id": "string",
        "type": "calc",
        "formula": "string",
    }).json(indent=2), "\n")

    try:
        Tag.parse_obj({
            "id": "string",
            "type": "calc",
            "source": "string",
            "source_path": "string",
        })
    except ValidationError as err:
        print(err.json(), "\n")

    try:
        Tag.parse_obj({
            "id": "string",
            "type": "initial",
            "formula": "string",
            "source": "string",
        })
    except ValidationError as err:
        print(err.json())

Output:

{
  "id": "string",
  "type": "calc",
  "formula": "string",
  "source": null,
  "source_path": null
}
[
  {
    "loc": [
      "__root__"
    ],
    "msg": "`calc` requires `formula`",
    "type": "value_error"
  }
] 
[
  {
    "loc": [
      "__root__"
    ],
    "msg": "`initial` requires `source` & `source_path`",
    "type": "value_error"
  }
]

Depending on what makes sense in your case, you may want to define some other (type of) default value for those conditional/optional fields. None is just the typical way to go, but you can use whatever you want of course. You just need to make sure to perform the correct checks for those default values inside your root_validator method.

  • Related