Inheritance and Polymorphism support #8243
-
|
Actually, there is no support for Inheritance and Polymorphism as I know in fastapi : https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/ Maybe annotations like that will do the job : class Item(BaseModel):
type: str
class CarItem(Item):
type: str
class PlaneItem(Item):
type: str
size: str
@app.post("/items/", response_model=Item, one_of=[CarItem, PlaneItem], discriminator="type")
async def create_item(*, item: Item):
return itemFor now, we need to define an I'll try to hack using the based starlette api : https://www.starlette.io/schemas/ but comment are unused by fastapi (except for description) |
Beta Was this translation helpful? Give feedback.
Replies: 23 comments
-
|
You can achieve what I think you need with standard Python I just added docs documenting it: https://fastapi.tiangolo.com/tutorial/extra-models/#union-or-anyof |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for the response !!! But I also try with request body : class Item(BaseModel):
type: str
class CarItem(Item):
type: str
wheel: int
class PlaneItem(Item):
type: str
size: str
item_union = Union[
CarItem,
PlaneItem
]
@app.post("/items/", response_model=item_union)
async def create_item(*, item: item_union):
return itemseems to pick the first one, so drop the fields that is not on the second one. Any suggestion for this one ? |
Beta Was this translation helpful? Give feedback.
-
|
confirmed with a set of tests, specifically |
Beta Was this translation helpful? Give feedback.
-
|
After some digging, it seems like it comes from upstream As soon as one So the CarItem in my test returns first as it's validated 1st. No idea if that's something to report upstream, that seems logic to consider valid a union as soon as one of its members is valid Maybe FastAPI should, if there are subfields, loop orderly on them and validate the body from there. |
Beta Was this translation helpful? Give feedback.
-
|
I think this case must be handle by fastapi. Since pydantic seems to be "compliant" with jsonchema and jsonschema don't support true polymorphism : anyOf, allOf, but no discriminator like OpenApi support. It seems to be more OpenApi feature than JsonSchema feature IMO. |
Beta Was this translation helpful? Give feedback.
-
|
So, in Pydantic, the order of Nevertheless, I tested it with your example and it seems to work well (as I expected it). When I send: {"type": "plane", "size": "big"}I receive that back. And when I send: {"type": "car", "wheel": 3}I receive that back too. What seems to be the problem there?
Maybe I'm not understanding this phrase well? Can you elaborate? |
Beta Was this translation helpful? Give feedback.
-
|
You are right ! I used a simplify example for the issue but this one works like that ! I little bit tricky, but it works class Item(BaseModel):
type: str
class CarItem(Item):
pass
class PlaneItem(Item):
plane: int
class TruckItem(Item):
truck: int
item_union = Union[
CarItem,
PlaneItem,
TruckItem
]
class Container(BaseModel):
collec: List[item_union]
@app.post("/items/", response_model=Container)
async def create_item(*, item: Container):
return item |
Beta Was this translation helpful? Give feedback.
-
|
Great! |
Beta Was this translation helpful? Give feedback.
-
|
it fails on my test branch on the it passes that route has the beow response model: you get effectively a plane back, but the |
Beta Was this translation helpful? Give feedback.
-
|
I've dig my code today and I understand my problem : class Item(BaseModel):
type: str
class CarItem(Item):
car: Optional[int]
class PlaneItem(Item):
plane: Optional[int]
class TruckItem(Item):
truck: Optional[int]
item_union = Union[
CarItem,
PlaneItem,
TruckItem
]
class Container(BaseModel):
collec: List[item_union]
@app.post("/items/", response_model=Container)
async def create_item(*, item: Container):
return itemAll the fields are optional, so the first always win. First one works well : But this one : return : {
"collec": [
{
"type": "truck",
"plane": null
}
]
}In this case of schema, I don't have any solution. It's a normal behavior for my app to have some optional field. Pydantic will always return the first one since it's valid and drop field for the second one. Now, I have to change the code to make the abstract class always win and keep fields : class Item(BaseModel):
type: str
class Config:
extra = Extra.allow
item_union = Union[
Item,
CarItem,
PlaneItem,
TruckItem
]The response will have extra fields, and I recreate the correct object on my controller. |
Beta Was this translation helpful? Give feedback.
-
|
Cool, I think you found a good solution for your use case. |
Beta Was this translation helpful? Give feedback.
-
|
I'll close this issue now, but feel free to add more comments or create new issues. |
Beta Was this translation helpful? Give feedback.
-
|
@tiangolo: What about the lack of a EDIT: Looks like Pydantic uses |
Beta Was this translation helpful? Give feedback.
-
|
I think real inheritance and polymorphism support would be very useful. Comments above show that using |
Beta Was this translation helpful? Give feedback.
-
The problem is just how schema validation works. With the example given in the original post, a "Real" inheritance and polymorphism is not really a concept in OpenAPI and JSON Schema validation (which is what FastAPI relies on to parse JSON into objects), since the data you receive is just an arbitrary JSON object, and the type of that object can only be determined by validating it against object schemas. There's just not much else to do when your object happens to match more than one. |
Beta Was this translation helpful? Give feedback.
-
|
Polymorphism is part of openapi : https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/ The We have succeed to have polymorphism in a fastapi application but with a lot of tricky things that would be simpler with support on webserver. |
Beta Was this translation helpful? Give feedback.
-
|
For what it's worth, I've raised an issue upstream (pydantic/pydantic#619) regarding making Support for I'm hoping |
Beta Was this translation helpful? Give feedback.
-
|
Support for it actually landed in the openapi-generator project for typescript, go, python etc this past release. So there is movement in the right direction. |
Beta Was this translation helpful? Give feedback.
-
|
Hi! @tchiotludo from typing import Union
from fastapi.applications import FastAPI
from pydantic import BaseModel
class Human(BaseModel):
age: int
name: str
class Man(Human):
""" man """
something: str
class Woman(Human):
""" woman """
class ManCreateRequest(Man):
class Config:
schema_extra = {
"example": {
"target": Man(age=29, name="deo", something="something").dict()
}
}
class HumanCreateRequest(BaseModel):
target: Union[Woman, Man]
class Config:
schema_extra = Man.Config.schema_extra # default Schema Man
app = FastAPI()
@app.post("/human")
def add_human(create_request: HumanCreateRequest) -> None:
instance_type = create_request.target # always Woman
returnPydantic will always return the first one... I want to get real type of |
Beta Was this translation helpful? Give feedback.
-
|
@mcauto: Ah, that's a common mistake people make. Pydantic can't tell what data structure your JSON is supposed to be aside from matching it against one of your possible models. As it happens, since If you really want the absence of class Woman(Human):
""" woman """
class Config:
extra = 'forbid'(I have not tested it myself, though, so apologies if this doesn't work) |
Beta Was this translation helpful? Give feedback.
-
@sm-Fifteen Thank you for the kind explanation! I solved it! from typing import Union
from fastapi.applications import FastAPI
from pydantic import BaseModel
from pydantic.main import Extra
class Human(BaseModel):
age: int
name: str
class Man(Human):
""" man """
man_something: str
class Woman(Human):
""" woman """
woman_something: str
class ManCreateRequest(Man):
class Config:
extra = Extra.forbid
schema_extra = {
"example": {
"target": Man(age=29, name="deo", man_something="man").dict()
}
}
class WomanCreateRequest(Woman):
class Config:
extra = Extra.forbid
schema_extra = {
"example": {
"target": Woman(
age=29, name="woorr", woman_something="woman"
).dict()
}
}
class HumanCreateRequest(BaseModel):
target: Union[WomanCreateRequest, ManCreateRequest]
class Config:
# schema_extra = ManCreateRequest.Config.schema_extra
schema_extra = WomanCreateRequest.Config.schema_extra
app = FastAPI()
@app.post("/human")
def add_human(create_request: HumanCreateRequest) -> str:
if isinstance(create_request.target, WomanCreateRequest):
return "woman"
elif isinstance(create_request.target, ManCreateRequest):
return "man"
else:
raise NotImplementedError() |
Beta Was this translation helpful? Give feedback.
-
|
Not sure if it's wasn't available at the time of writing but I think this is a simpler solution: from typing import List, Union
from pydantic import BaseModel
class BaseItem(BaseModel):
description: str
type: str
class CarItem(BaseItem):
wheels: int
@validator('type')
def check_type(cls, v):
if v != "car":
raise ValueError("invalid type")
return v
class PlaneItem(BaseItem):
size: int
@validator('type')
def check_type(cls, v):
if v != "plane":
raise ValueError("invalid type")
return v
class Container(BaseModel):
__root__: Union[PlaneItem, CarItem]
container = Container.parse_obj({"description": "All my friends drive a low rider", "type": "car"})
container2 = Container.parse_obj({"description": "Music is my aeroplane, it's my aeroplane",
"type": "plane",
"size": 5})
print(container.__root__) # car object
print(container2.__root__) # plane object |
Beta Was this translation helpful? Give feedback.
-
|
Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs. |
Beta Was this translation helpful? Give feedback.

Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs.