Pydantic: сделать поле None в валидаторе основанным на значении другого поля

Я пользуюсь пидантиком BaseModel с валидатором, как это:

from datetime import date
from typing import List, Optional
from pydantic import BaseModel, BaseConfig, validator

class Model(BaseModel):
    class Config(BaseConfig):
        allow_population_by_alias = True
        fields = {
            "some_date": {
                "alias": "some_list"
            }
        }
    some_date: Optional[date]
    some_list: List[date]

    @validator("some_date", pre=True, always=True)
    def validate_date(cls, value):
        if len(value) < 2: # here value is some_list
            return None
        return value[0] # return the first value - let's assume it's a date string

# This reproduces the problem
m = Model(some_list=['2019-01-03'])

Я хотел бы рассчитать стоимость some_date основанный на значении some_list и сделать это None если определенное условие выполнено.

Мой JSON никогда не содержит поле some_dateвсегда заселен на основе some_list следовательно pre=True, always=True, Однако валидатор по умолчанию для some_date будет работать после моего пользовательского, который потерпит неудачу, если validate_date возвращается None,

Есть ли способ создать такое поле, которое вычисляется только другим и все еще может быть Optional?

4 ответа

Решение

Я склонен сказать, что это невозможно, как вы пытаетесь.

Вы правильно указали, что валидатор по умолчанию для date называется после обычая validate_date из вашей модели данных. Соответствующий исходный код можно найти здесь: pydantic.fields.py (по состоянию на январь 2019 г., git-hash: 19320bf). По-видимому, allow_none -мод не поддерживается для каскада валидаторов, см. pydantic.Field._apply_validators для деталей. Более конкретно, вывод конкретного валидатора никогда не проверяется на None этот тест проводится дальше по течению Model.validate,

Исходя из чтения документации и источника pydantic, я склонен сказать, что механизм проверки pydantic в настоящее время имеет очень ограниченную поддержку для преобразования типов (list -> date, list -> NoneType) в рамках функций проверки. Если у вас есть веские аргументы для вашего варианта использования, вы можете запросить эту функцию здесь.

Делая шаг назад, однако, ваш подход с использованием alias и флаг allow_population_by_alias (что не рекомендуется, в любом случае, как указано в документации) кажется немного перегруженным. some_date нужен только как ярлык для some_list[0] if len(some_list) >= 2 else None, но это никогда не устанавливается независимо от some_list, Если это действительно так, то почему бы не выбрать следующий, гораздо более простой вариант?

class Model(BaseModel):
    some_list: List[date] = ...

    @property 
    def some_date(self):
        return None if len(self.some_list) < 2 else self.some_list[0]

Если вы хотите иметь возможность динамически изменять поле в соответствии с другим, вы можете использовать valuesаргумент. Он содержит все предыдущие поля и осторожно: порядок имеет значение. Вы можете сделать это либо с помощью validator или root_validator.

С validator

>>> from datetime import date
>>> from typing import List, Optional
>>> from pydantic import BaseModel, validator
>>> class Model(BaseModel):
        some_list: List[date]
        some_date: Optional[date]
    
        @validator("some_date", always=True)
        def validate_date(cls, value, values):
            if len(values["some_list"]) < 2:
                return None
            return values["some_list"][0]

>>> Model(some_list=['2019-01-03', '2020-01-03', '2021-01-03'])
Model(some_list=[datetime.date(2019, 1, 3), datetime.date(2020, 1, 3), datetime.date(2021, 1, 3)],
      some_date=datetime.date(2019, 1, 3))

Но, как я уже сказал, если вы поменяете порядок some_list и some_date, у вас будет KeyError: 'some_list'!

С root_validator

Другой вариант - использовать root_validator. Они действуют во всех сферах:

>>> class Model(BaseModel):
        some_list: List[date]
        some_date: Optional[date]
    
        @root_validator
        def validate_date(cls, values):
            if not len(values["some_list"]) < 2:
                values["some_date"] = values["some_list"][0]
            return values

>>> Model(some_list=['2019-01-03', '2020-01-03', '2021-01-03'])
Model(some_list=[datetime.date(2019, 1, 3), datetime.date(2020, 1, 3), datetime.date(2021, 1, 3)],
      some_date=datetime.date(2019, 1, 3))

Вы должны уметь использовать valuesсогласно pydantic docs

вы также можете добавить в подпись любое подмножество следующих аргументов (имена должны совпадать):

значения: словарь, содержащий сопоставление имени и значения любых ранее проверенных полей

config: конфигурация модели

field: проверяемое поле

**kwargs: если предоставлено, это будет включать в себя аргументы выше, явно не указанные в подписи

@validator()
def set_value_to_zero(cls, v, values):
    # look up other value in values, set v accordingly.

Как насчет переопределения__init__?

      from datetime import date
from typing import List, Optional
from pydantic import BaseModel

class Model(BaseModel):
    some_date: Optional[date]
    some_list: List[date]

    def __init__(self, *args, **kwargs):

        # Modify the arguments
        if len(kwargs['some_list']) < 2:
            kwargs['some_date'] = None
        else:
            kwargs['some_date'] = kwargs['some_list'][0]

        # Call parent's __init__
        super().__init__(**kwargs)

Model(some_list=['2019-01-03', '2022-01-01'])
# Output: Model(some_date=datetime.date(2019, 1, 3), some_list=[datetime.date(2019, 1, 3), datetime.date(2022, 1, 1)])

Обратите внимание, что если вы изменяете экземпляр после создания, эта проверка не выполняется.

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