4

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?

KOB
  • 4,084
  • 9
  • 44
  • 88

2 Answers2

3

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.

Hernán Alarcón
  • 3,494
  • 14
  • 16
  • It is worth adding that if the private key is not set, get_address is still called, and will result in an undesirable exception (instead of a validation error). Changing this line: `if address == "":` To: `if address == "" and 'private_key' in values:` Will help with that – Adam Feb 02 '23 at 11:34
  • @Adam, thanks for the suggestion, I added it. I agree with you that it is always better to check if a given key is in the `values` dict because any previously validated field could be invalid. In this particular case `private_key` has a `default_factory`, so the `KeyError` will be caused not by not setting it, but by setting it with an invalid value (something that pydantic cannot parse as a `str`). – Hernán Alarcón Feb 04 '23 at 04:22
0

I was able to acheive it using a root_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 acheieve this though

KOB
  • 4,084
  • 9
  • 44
  • 88