У меня есть JSON со структурой, похожей на показанную ниже. Список threshold
представляет объекты, тип которых может быть "type": "upper_limit"
или "type": "range"
. Обратите внимание, что значение "target"
должно быть целым числом или числом с плавающей запятой в зависимости от типа объекта.
{
"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 для создания схемы JSON для приведенных выше данных приведены ниже:
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]
Модели проверяют JSON, но ограничения для значения target
для этого типа игнорируются. Например, если пороговый объект содержит "type": "range", "target": 12,
, то ошибки не выдаются, поскольку он анализируется как целое число и, следовательно, используются ограничения, определенные ThresholdUpperLimit
; но следует использовать ограничения, определенные ThresholdRange
, поскольку тип "range"
. Есть какие-нибудь предложения о том, как правильно с этим справиться?
Самый распространенный и наиболее разумный подход — реализовать собственный метод «model_validate
» для построения схемы на основе предоставленного словаря.
Следует добавить несколько проверок на KeyError
, но идея такова:
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)
Ваша логика достаточно индивидуальна, поэтому не бойтесь реализовать для нее собственный метод. Это сделает ваше решение намного более стабильным и масштабируемым.
Мне удалось обеспечить правильное разрешение модели, изменив использование подклассов перечисления на использование литерала.
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]
Однако это не обязывает target
быть float
в ThresholdRange
классе. int
пройдет.