Describe the bug
The Union type works as expected when response model is defined as such according to docs: https://fastapi.tiangolo.com/tutorial/extra-models/#union-or-anyof However when parameters (body payload) are defined as Union the code runs fine until executing method, and docs are picking up the types and generating schema correctly but the empty ValidationRequestError is thrown when route method is called even if there are all parameters sent are valid.
To Reproduce
Sample code to reproduce:
class SimpleData(BaseModel):
foo: Optional[str] = None
class ExtendedData(SimpleData):
bar: str # Note that this is required
PostData = Union[ExtendedData, SimpleData]
@router.post("/test")
async def post(data: PostData):
return "OK"
Then the POST /test route is called with a body payload:
{
"foo": "test1",
"bar": "test2"
}
As a result the empty ValidationRequestError is thrown with value_error.missing message but no actual field assigned to it.
Expected behavior
Parameters from the request are resolved and parsed against the types inside Union.
Environment:
0.29.0just ran quickly this and it passes fine so I may have misunderstood what goes wrong ?
from typing import Optional, Union
from pydantic import BaseModel
from starlette.testclient import TestClient
from fastapi import FastAPI
class SimpleData(BaseModel):
foo: Optional[str] = None
class ExtendedData(SimpleData):
bar: str # Note that this is required
PostData = Union[ExtendedData, SimpleData]
app = FastAPI()
client = TestClient(app)
@app.post("/testunion")
async def testunion(data: PostData):
print(data)
return "ok"
def test_union():
data = PostData(foo="test1", bar="test2")
response = client.post("/testunion", json=data.dict())
assert response.status_code == 200
assert response.json() == "ok"
data = {"foo": "test1", "bar": "test2"}
response = client.post("/testunion", json=data)
assert response.status_code == 200
assert response.json() == "ok"
I'm following exactly the same pattern in my code and it doesn't work. Error is somehow thrown when request is parsed.
My models look like this:
from pydantic import BaseModel, condecimal, Decimal
from datetime import datetime
class FetcherPatchSimpleData(BaseModel):
retries: condecimal(gt=Decimal(0)) = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
class FetcherPatchAdminData(FetcherPatchSimpleData):
status: Optional[str] = None
active: Optional[bool] = None
FetcherPatchData = Union[FetcherPatchAdminData, FetcherPatchSimpleData]
Then as for route:
@app.patch("/fetchers/{fetcher_id}")
async def patch(*,
fetcher_id: UUID,
data: FetcherPatchData):
# some app logic (never gets here)
return "OK"
As a result exception is thrown:

If I change data: FetcherPatchData to data: FetcherPatchSimpleData or data: FetcherPatchAdminData everything works fine.
mmm this passes fine as well, just changed a patch to a post and removed the uuid
Would you share an example of what you actually pass as FetcherPatchData object ?
I had to use json.loads(model.json()) instead of model.dict() in the tests because of Decimal and datetime json serialization, maybe you're hit by that before you think ?
import json
from datetime import datetime
from typing import Optional, Union
from pydantic import BaseModel, condecimal, Decimal, ConstrainedDecimal, conint
from starlette.testclient import TestClient
from fastapi import FastAPI
class SimpleData(BaseModel):
foo: Optional[str] = None
class ExtendedData(SimpleData):
bar: str # Note that this is required
PostData = Union[ExtendedData, SimpleData]
class FetcherPatchSimpleData(BaseModel):
retries: condecimal(gt=0) = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
class FetcherPatchAdminData(FetcherPatchSimpleData):
status: Optional[str] = None
active: Optional[bool] = None
FetcherPatchData = Union[FetcherPatchAdminData, FetcherPatchSimpleData]
app = FastAPI()
client = TestClient(app)
@app.post("/testunion")
async def testunion(data: PostData):
print(data)
return "ok"
@app.post("/fetchers")
async def test_fetchers(data: FetcherPatchData):
# some app logic (never gets here)
return "OK"
def test_union():
data = PostData(foo="test1", bar="test2")
response = client.post("/testunion", json=data.dict())
assert response.status_code == 200
assert response.json() == "ok"
data = {"foo": "test1", "bar": "test2"}
response = client.post("/testunion", json=data)
assert response.status_code == 200
assert response.json() == "ok"
def test_fetchers():
fpad = FetcherPatchAdminData(status="s1", active=True)
response = client.post("/fetchers", json=fpad.dict())
assert response.status_code == 200
assert response.json() == "OK"
start_date = datetime(2018,12,2)
end_date = datetime(2019,1,2)
fpsd = FetcherPatchSimpleData(retries=11, start_date=start_date, end_date=end_date)
response = client.post("/fetchers", json=json.loads(fpsd.json()))
assert response.status_code == 200
assert response.json() == "OK"
I tried to change my code, but nothing works like that and I' still getting the error in any case. As a workaround I could force resolving Union by providing source of parameters explicitly:
@app.patch("/fetchers")
async def test_fetchers(data: FetcherPatchData = Body(...)):
# some app logic (never gets here)
return "OK"
Only then Union is parsed correctly, otherwise FastAPI looks up the data parameter in query string parameters it seems. For unknown reason it works fine if the type is not an Union but for example FetcherPatchSimpleData. Not exactly how it should be, right?
Would you share an example of what you actually pass as
FetcherPatchDataobject ?
I send just a json payload, for example:
{
"retries": 3,
"active": true
}
@LKay please write a simple self-contained app that doesn't pass and shows your error. Something that can be run as is.
That way we would be able to check it and help you debug it or discover the issue.
I made a self-contained (modified) version of your original example and seems to be working fine:
from typing import Union, Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class SimpleData(BaseModel):
foo: Optional[str] = None
class ExtendedData(SimpleData):
bar: str # Note that this is required
PostData = Union[ExtendedData, SimpleData]
@app.post("/test")
async def post(data: PostData):
return "OK"
Also, note that there have been frequent releases with new features and bug fixes. So, it might be that you have an older version of FastAPI and that the error might disappear once you update.
I've encountered the same issue when trying to post to an endpoint meant to handle unions. Modified @tiangolo's example above slightly:
#!/usr/bin/env python3.7
import logging
import sys
from typing import Union, Optional
import fastapi
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
handler = logging.StreamHandler(sys.stdout)
logger = logging.getLogger('fastapi')
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.debug(f'FastAPI v{fastapi.__version__}')
app = FastAPI()
app.debug = True
client = TestClient(app)
class SimpleData(BaseModel):
foo: Optional[str] = None
class ExtendedData(SimpleData):
bar: str # Note that this is required
PostData = Union[ExtendedData, SimpleData]
@app.post("/test")
def post(data: PostData):
logger.debug("Never reaches here.")
return "OK"
def test_union():
data = ExtendedData(bar='test1', foo='test2')
response = client.post("/test", json=data.dict())
logger.debug(response)
logger.debug(response.reason)
logger.debug(response.text)
test_union()
Logging output:
FastAPI v0.30.0
<Response [422]>
Unprocessable Entity
{"detail":[{"loc":["query","data"],"msg":"field required","type":"value_error.missing"}]}
I did expect to get more debug logging - am I missing something when setting up logging?
@nckswt that's interesting, FastAPI is thinking that the data is a query parameter. I think that's a FastAPI bug on its own. But meanwhile, you can force FastAPI to think it's a body by using Body(...).
Interesting! Yup, can confirm that
@app.post("/test")
def post(data: PostData = Body(...)):
return "OK"
Does return successfully.
I just created The PR which fix this issue.
This was fixed by @koxudaxi in #400. :tada: :rocket:
Something to have in mind, when using Union in Python 3.6:
If you have a model ExtendedData that inherits from SimpleData, and a type Union[ExtendedData, SimpleData], Python 3.6 will convert that type to just SimpleData.
To use a Union with a model that inherits from another you need Python 3.7.
This seems to be fixed, so closing to clean up backlog.