KOB
KOB

Reputation: 4545

Setting a default value based on another value

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

Answers (4)

KOB
KOB

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

Steve Williams
Steve Williams

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

richin13
richin13

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

Hernán Alarcón
Hernán Alarcón

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:

  1. 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.

  2. 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

Related Questions