I'm using the pydantic BaseModel
with a validator like this:
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'])
I would like to compute the value of some_date
based on the value of some_list
and make it None
if a certain condition met.
My JSON never contains the field some_date
, it's always populated based on some_list
hence pre=True, always=True
. However the default validator for some_date
will run after my custom one, which will fail if validate_date
returns None
.
Is there a way to create such a field which is only computed by another one and still can be Optional
?
If you want to be able to dynamically modify a field according to another one, you can use the values
argument. It holds all the previous fields, and careful: the order matters. You could do this either using a validator
or a 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))
But as I said if you exchange the order of some_list
and some_date
, you will have a KeyError: 'some_list'
!
root_validator
Another option would be to use a root_validator
. These act on all fields:
>>> 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))
Update: As others pointed out, this can be done now with newer versions (>=0.20). See this answer. (Side note: even the OP's code works now, but doing it without alias is even better.)
From skim reading documentation and source of pydantic, I tend to to say that pydantic's validation mechanism currently has very limited support for type-transformations (list -> date
, list -> NoneType
) within the validation functions.
Taking a step back, however, your approach using an alias
and the flag allow_population_by_alias
seems a bit overloaded. some_date
is needed only as a shortcut for some_list[0] if len(some_list) >= 2 else None
, but it's never set independently from some_list
. If that's really the case, why not opting for the following option?
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With