11

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()
Yagiz Degirmenci
  • 16,595
  • 7
  • 65
  • 85
Honza Sedloň
  • 358
  • 10
  • 27

2 Answers2

9

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

kigawas
  • 1,153
  • 14
  • 27
  • 2
    What if I want to apply this technique to FastAPI Request body/params? I tried it but it doesn't seem to discriminate between the types – Beast Aug 26 '22 at 08:09
  • 1
    @Beast That's not a good idea to mix different parameters in the same endpoint. If you insist on doing so, I suggest you to combine the fields into one model and write methods to convert them separately. – kigawas Dec 21 '22 at 07:41
1

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

hege
  • 126
  • 1
  • 2