Reputation: 4545
I have the following Pydantic model:
from pydantic import BaseModel
import key
class Wallet(BaseModel):
private_key: str = Field(default_factory=key.generate_private_key)
address: str
I want address to have a default_factory as a function that takes a private_key as input and returns an address. My intentions would be something along the lines of the following:
address: str = Field(default_factory=key.generate_address(self.private_key)
How can I achieve this?
Upvotes: 9
Views: 9308
Reputation: 4545
With Pydantic v1, I was able to achieve it using a root_validator
. Note that Pydantic v2 deprecated root_validator
in favor of model_validator
.
class Wallet(BaseModel):
address: str = None
private_key: str = Field(default_factory=key.generate_private_key)
@root_validator
def get_address(cls, values) -> dict:
if values.get("address") is None:
values["address"] = key.generate_address(values.get("private_key"))
return values
Not sure if this is the best way to achieve this though
Upvotes: 2
Reputation: 66
Further to Hernán's answer (apologies, I do not have enough reputation to edit or comment on it!) this answer is perfect, however in Pydantic 2.0 the @validator
decorator is deprecated.
To achieve what I wanted, I used the @model_validator
decorator instead with the after
mode.
So in this example:
class Wallet(BaseModel):
private_key: str = Field(default_factory=key.generate_private_key)
address: str = "" # "" seems better than None to use the correct type
@model_validator(mode="after")
def set_default_address(self):
if self.address == "" and self.private_key is not None:
self.address = key.generate_address(self.private_key)
return self
Upvotes: 1
Reputation: 47
For Pydantic v2 users you can use a computed_field
decorator to achieve this behavior:
from pydantic import BaseModel, computed_field
import key
class Wallet(BaseModel):
private_key: str = Field(default_factory=key.generate_private_key)
@computed_field()
def address(self) -> str:
return self.private_key
More details on pydantic.fields.computed_field
Upvotes: 3
Reputation: 4099
Another option is to just use @validator
because in it you can access previously validated fields. From the documentation:
- validators are "class methods", so the first argument value they receive is the UserModel class, not an instance of UserModel.
- the second argument is always the field value to validate; it can be named as you please
- you can also add any subset of the following arguments to the signature (the names must match):
values
: a dict containing the name-to-value mapping of any previously-validated fields
Example:
class Wallet(BaseModel):
private_key: str = Field(default_factory=key.generate_private_key)
address: str = "" # "" seems better than None to use the correct type
@validator("address", always=True)
def get_address(cls, address: str, values: Dict[str, Any]) -> str:
if address == "" and "private_key" in values:
return key.generate_address(values["private_key"])
return address
It can be argued that @validator
should be preferred over @root_validator
if you just want to generate a value for a single field.
There are two important aspects of this approach that must be considered:
The "previously-validated fields" from the documentation means that in your case private_key
must be defined before address
. The values of fields defined after the field that is validated are not available to the validator.
If the field that is validated has a default value and you still want the validator to be executed in that case, you have to use always=True
.
Upvotes: 6