21

Say I have model

class UserDB(BaseModel):
    first_name: Optional[str] = None
    last_name: Optional[str] = None

How do I make another model that is constructed from this one and has a field that changes based on the fields in this model?

For instance, something like this

class User(BaseModel):
    full_name: str = first_name + ' ' + last_name

Constructed like this maybe

User.parse_obj(UserDB)

Thanks!

3 Answers3

22

If you do not want to keep first_name and last_name in User then you can

  • customize __init__.
  • use validator for setting full_name.

Both methods do what you want:

from typing import Optional
from pydantic import BaseModel, validator


class UserDB(BaseModel):
    first_name: Optional[str] = None
    last_name: Optional[str] = None


class User_1(BaseModel):
    location: str  # for a change
    full_name: Optional[str] = None

    def __init__(self, user_db: UserDB, **data):
        super().__init__(full_name=f"{user_db.first_name} {user_db.last_name}", **data)


user_db = UserDB(first_name="John", last_name="Stark")
user = User_1(user_db, location="Mars")
print(user)


class User_2(BaseModel):
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    full_name: Optional[str] = None

    @validator('full_name', always=True)
    def ab(cls, v, values) -> str:
        return f"{values['first_name']} {values['last_name']}"


user = User_2(**user_db.dict())
print(user)

output

location='Mars' full_name='John Stark'
first_name='John' last_name='Stark' full_name='John Stark'

UPDATE: For working with response_model you can customize __init__ in such way:

class User_1(BaseModel):
    location: str  # for a change
    full_name: Optional[str] = None

    # def __init__(self, user_db: UserDB, **data):
    def __init__(self, first_name, last_name, **data):
        super().__init__(full_name=f"{first_name} {last_name}", **data)


user_db = UserDB(first_name="John", last_name="Stark")
user = User_1(**user_db.dict(), location="Mars")
print(user)
alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • This works under normal conditions, thanks! For bonus points, do you happen to know how to get this to work with the fastapi response_model? If I just return the db model with the response model set to the api model, it throws `pydantic.error_wrappers.ValidationError: 1 validation error for User response __init__() missing 1 required positional argument: 'user_db' (type=type_error)` – Florian van Kampen-Wright Aug 19 '20 at 19:30
  • 11
    I'm using the 2nd approach in my project, but i've always felt that using a validator to set a property just seems ...dirty. I also played around with adding @property but then it gets excluded from `.dict()` method – ierdna Dec 02 '20 at 03:23
4

I created a pip package that seems to do exactly what you need. Here is the link: https://pypi.org/project/pydantic-computed/

Your example would then look like this:

from pydantic import BaseModel
from pydantic_computed import Computed, computed

class UserDB(BaseModel):
    first_name: Optional[str] = None
    last_name: Optional[str] = None

class User(UserDB):
    full_name: Computed[str]

    @computed('full_name')
    def compute_full_name(first_name: str, last_name: str):
        return first_name + ' ' + last_name


# parsing also works as normal:
user_db = UserDB(first_name='John', last_name='Doe')
user = User.parse_obj(user_db)
print(user.full_name) # Outputs "John Doe"

This will also work for response_model (e.g. in FastAPI) since the computed value is actually set on the full_name property.

0

Please use at least pydantic==2.0. Then you could use computed_field from pydantic.

from pydantic import BaseModel, computed_field


class UserDB(BaseModel):
     first_name: Optional[str] = None
     last_name: Optional[str] = None

     @computed_field
     def full_name(self) -> str:
         return f"{self.first_name} {self.last_name}"

Then you should get:

print(UserDB(first_name="John", last_name="Doe").model_dump())
#> {'first_name': 'John, 'last_name': 'Doe', 'full_name': 'John Doe'}
ikreb
  • 2,133
  • 1
  • 16
  • 35