I know a concept from Typescript called Discriminated unions. That's a thing where you put 2 struct (classes, etc) and the type is decided depending on a struct's values. I'm trying to achieve similar thing in FastAPI with Pydantic validation. There are two different request payloads that I can recieve. Whether it's one or another depends on accountType variable. If it's creative it should be validated by RegistrationPayloadCreative and if it's brand it should validate by RegistrationPayloadBrand. How do I achieve this? Couldn't find any other solution.
The problem is that it either returns
unexpected value; permitted: 'creative' (type=value_error.const; given=brand; permitted=('creative',))
Or it doesn't work at all.
class RegistrationPayloadBase(BaseModel):
first_name: str
last_name: str
email: str
password: str
class RegistrationPayloadCreative(RegistrationPayloadBase):
accountType: Literal['creative']
class RegistrationPayloadBrand(RegistrationPayloadBase):
company: str
phone: str
vat: str
accountType: Literal['brand']
class A(BaseModel):
b: Union[RegistrationPayloadBrand, RegistrationPayloadCreative]
def main():
A(b={'first_name': 'sdf', 'last_name': 'sdf', 'email': 'sdf', 'password': 'sdfds', 'accountType': 'brand'})
if __name__ == '__main__':
main()
You should use __root__ and parse_obj instead.
from typing import Union
from pydantic import BaseModel
class PlanetItem(BaseModel):
id: str
planet_name: str
# ...
class CarItem(BaseModel):
id: str
name: str
# ...
class EitherItem(BaseModel):
__root__: Union[PlanetItem, CarItem]
@app.get("/items/{item_id}", response_model=EitherItem)
def get_items(item_id):
return EitherItem.parse_obj(response) # Now you get either PlanetItem or CarItem
Credit: https://github.com/tiangolo/fastapi/issues/2279#issuecomment-787517707
The error message is a bit misleading since the problem is that company/phone/vat fields are mandatory in RegistrationPayloadBrand.
So:
>>> A(b={'first_name': 'sdf', 'last_name': 'sdf', 'email': 'sdf', 'password': 'sdfds', 'accountType': 'brand', 'company':'foo','vat':'bar', 'phone':'baz'})
A(b=RegistrationPayloadBrand(first_name='sdf', last_name='sdf',
email='sdf', password='sdfds', company='foo', phone='baz', vat='bar', accountType='brand'))
>>> A(b={'first_name': 'sdf', 'last_name': 'sdf', 'email': 'sdf', 'password': 'sdfds', 'accountType': 'creative'})
A(b=RegistrationPayloadCreative(first_name='sdf', last_name='sdf', email='sdf', password='sdfds', accountType='creative'))
Or making them Optional (if payload not necessarily contains those fields)
class RegistrationPayloadBrand(RegistrationPayloadBase):
company: Optional[str]
phone: Optional[str]
vat: Optional[str]
accountType: Literal['brand']
>>> A(b={'first_name': 'sdf', 'last_name': 'sdf', 'email': 'sdf', 'password': 'sdfds', 'accountType': 'brand'})
A(b=RegistrationPayloadBrand(first_name='sdf', last_name='sdf', email='sdf', password='sdfds', company=None, phone=None, vat=None, accountType='brand'))
>>> A(b={'first_name': 'sdf', 'last_name': 'sdf', 'email': 'sdf', 'password': 'sdfds', 'accountType': 'creative'})
A(b=RegistrationPayloadCreative(first_name='sdf', last_name='sdf', email='sdf', password='sdfds', accountType='creative'))
will solve the problem