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)])
Обратите внимание, что если вы изменяете экземпляр после создания, эта проверка не выполняется.