Определите ограничения для различных моделей схемы JSON в списке

У меня есть 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". Есть какие-нибудь предложения о том, как правильно с этим справиться?

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Самый распространенный и наиболее разумный подход — реализовать собственный метод «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 пройдет.

Другие вопросы по теме