skip to Main Content

I have some JSON with a structure similar to what is shown below. The threshold list represents objects where the type can be "type": "upper_limit" or "type": "range". Notice the "target" value should be an integer or float depending on the type of the object.

{
    "name": "blah",
    "project": "blah blah",
    "threshold": [
        {
            "id": "234asdflkj",
            "group": "walkers",
            "type": "upper_limit",
            "target": 20,
            "var": "distance"
        },
        {
            "id": "asdf34asf2654",
            "group": "runners",
            "type": "range",
            "target": 1.7,
            "var": "speed"
        }
    ]
}

Pydantic models to generate a JSON schema for the above data are given below:

class ThresholdType(str, Enum):
    upper_limit = "upper_limit"
    range = "range"


class ThresholdUpperLimit(BaseModel):
    id: str
    group: str
    type: ThresholdType = "upper_limit"
    target: int = Field(gt=2, le=20)
    var: str


class ThresholdRange(BaseModel):
    id: str
    group: str
    type: ThresholdType = "range"
    target: float = Field(gt=0, lt=10)
    var: str


class Campaign(BaseModel):
    name: str
    project: str
    threshold: list[ThresholdUpperLimit | ThresholdRange]

The models validate the JSON, but the constraints for the target value are being ignored for the type. For example, if a threshold object contains "type": "range", "target": 12, then no errors are thrown because it is being parsed as an integer and therefore constraints defined by the ThresholdUpperLimit are used; but the constraints defined by ThresholdRange should be used because the type is "range". Any suggestions on how to properly handle this?

2

Answers


  1. The most common and most wise approach would be to implement a custom "model_validate" method to build the schema based on the provided dictionary.

    Some checks for KeyError should be added but the idea is:

    class ThresholdBase(BaseModel):
        id: str
        group: str
        var: str
    
    
    class ThresholdUpperLimit(ThresholdBase):
        type: ThresholdType = ThresholdType.upper_limit
        target: int = Field(gt=2, le=20)
    
    
    class ThresholdRange(ThresholdBase):
        type: ThresholdType = ThresholdType.range
        target: float = Field(gt=0, lt=10)
    
    
    class Campaign(BaseModel):
        name: str
        project: str
        threshold: list[ThresholdUpperLimit | ThresholdRange]
    
        @classmethod
        def from_dict(cls, campaign: dict) -> "Campaign":
            """Custom method to build a model based on dict provided."""
    
            threshold_class_map: dict[ThresholdType, Type[ThresholdBase]] = {
                ThresholdType.range: ThresholdRange,
                ThresholdType.upper_limit: ThresholdUpperLimit,
            }
    
            thresholds: list[ThresholdBase] = []
    
            for threshold in campaign["threshold"]:
                threshold_type = ThresholdType(threshold["type"])
                threshold_class = threshold_class_map[threshold_type]
    
                thresholds.append(threshold_class.model_validate(threshold))
    
            return cls(
                name=campaign["name"],
                project=campaign["project"],
                threshold=thresholds
            )
    
    campaign = Campaign.from_dict(body)
    
    Login or Signup to reply.
  2. I managed to enforce the correct model resolution by changing your use of enum subclassing to using a Literal.

    from pydantic import BaseModel, Field
    from typing import Literal
    
    
    class ThresholdUpperLimit(BaseModel):
        id: str
        group: str
        type: Literal["upper_limit"]
        target: int = Field(gt=2, le=20)
        var: str
    
    
    class ThresholdRange(BaseModel):
        id: str
        group: str
        type: Literal["range"]
        target: float = Field(gt=0, lt=10)
        var: str
    
    
    class Campaign(BaseModel):
        name: str
        project: str
        threshold: list[ThresholdUpperLimit | ThresholdRange]
    

    However, this does not force target to be a float on the ThresholdRange class. An int will pass.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search