Как использовать размеченные типы объединения при проверке тела FastAPI? (Союз по моделям)

Я знаю концепцию из Typescript под названием Дискриминационные союзы. Это то, что вы помещаете 2 структуры (классы и т. Д.), И тип определяется в зависимости от значений структуры. Я пытаюсь добиться того же в FastAPI с проверкой Pydantic. Есть две разные полезные нагрузки запроса, которые я могу получить. То ли это, то ли зависит от переменной accountType. Если это creative, он должен быть подтвержден RegistrationPayloadCreative, а если brand, он должен быть подтвержден RegistrationPayloadBrand. Как мне этого добиться? Не удалось найти другого решения.

Проблема в том, что он либо возвращает

unexpected value; permitted: 'creative' (type=value_error.const; given=brand; permitted=('creative',))

Или вообще не работает.

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()

person Honza Sedloň    schedule 22.07.2020    source источник


Ответы (2)


Сообщение об ошибке немного вводит в заблуждение, поскольку проблема в том, что поля компании / телефона / НДС являются обязательными в 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'))

Или сделать их необязательными (если полезная нагрузка не обязательно содержит эти поля)

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'))


решит проблему

person hege    schedule 10.09.2020
comment
Вы должны использовать __root__. Смотри мой ответ - person kigawas; 13.07.2021

Вместо этого вы должны использовать __root__ и parse_obj.

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

Кредит: https://github.com/tiangolo/fastapi/issues/2279#issuecomment-787517707

person kigawas    schedule 13.07.2021