fastapi
fastapi copied to clipboard
[BUG] Schema is generated using field alias
Describe the bug
Schema that is generated in FasAPI /docs
endpoint is generated with alias rather than the model field when in the route configuration it is specified the route will reply with the model fields instead of the model fields' alias. Route for endpoint is using response_model
to define the response schema. I tried setting response_model_by_alias
to False
which correctly uses the model field's instead of mode field aliases in the response however the schema is wrong in this case.
To Reproduce
Steps to reproduce the behavior with a minimum self-contained file.
Replace each part with your own scenario:
- Create a file with:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Transaction(BaseModel):
id: ObjectIdStr = Field(..., alias='_id')
loan_id: ObjectIdStr = Field(..., alias='loanId')
@app.get("/", response_model=Transaction, response_model_by_alias=False)
def read_root():
return {"Hello": "World"}
- Open the browser and call the endpoint
/docs
. - Check schema response vs the actual response and notice schema generated is inconsistent with the response.
Expected behavior
Schema should use the model fields instead of using model fields' alias when response_model_by_alias=False
is set.
Environment
- OS: Linux / Windows / macOS
- FastAPI Version: 0.44
- Python version: 3.8
@aleksarias , I cannot reproduce your error. What is the "ObjectIdStr" ?
@rappongy I believe what he's saying is if you have an alias in a model, the response model will transform the response using the alias regardless of the response_model_by_alias
value. I'm not sure ObjectIdStr
is relevant!
Anyone found a workaround for this?
I have found the cause of the issue in v. 0.52.0 but it can equally be found in the master branch.
The issue stems from not passing the by_alias
flag to the openapi creation code when the schemas for the app's endpoints are created in this function:
https://github.com/tiangolo/fastapi/blob/0.52.0/fastapi/utils.py#L83-L85
Using OPs code, it can be reproduced with:
client = TestClient(app)
response = client.get('/openapi.json')
While this bug creates a disconnect between FastAPI and Pydantic, from FastAPI's side, I think the end solution to handle schema aliasing might be better placed in Pydantic; e.g. through an additional property in model config.
Thanks for the discussion everyone.
response_model_by_alias
is mainly a hack/workaround for quick experiments, mainly because some people were asking for that, but I'm inclined to deprecating and removing it at some point. Its behavior is actually quite strange, and there's a much better way to solve the same problem.
More background on that here: https://github.com/tiangolo/fastapi/pull/1642 and here: https://github.com/tiangolo/fastapi/pull/264
For your specific use case, your code example doesn't really run, I modified it to what I imagine is your intention:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Transaction(BaseModel):
id: str = Field(..., alias="_id")
loan_id: str = Field(..., alias="loanId")
@app.get("/", response_model=Transaction, response_model_by_alias=False)
def read_root():
return {"_id": "foo", "loanId": "bar"}
And when you call that endpoint, you would receive:
{
"id": "foo",
"loan_id": "bar"
}
and then, you would want to have the JSON Schema inside OpenAPI declare the model with id
and loan_id
.
Now, notice that you return this in the function:
{"_id": "foo", "loanId": "bar"}
but you received this in the HTTP JSON response:
{
"id": "foo",
"loan_id": "bar"
}
Note: if you wanted to be able to return data using the field names and not the aliases you would need to use allow_population_by_field_name
.
How to do it
Assuming something like that is what you want to achieve while having proper OpenAPI and JSON Schema support, this is how you would implement it:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Transaction(BaseModel):
id: str = Field(..., alias="_id")
loan_id: str = Field(..., alias="loanId")
name: str # to show that you can reuse fields with inheritance
class TransactionResponse(Transaction):
id: str
loan_id: str
@app.get("/", response_model=TransactionResponse)
def read_root():
return {"id": "foo", "loan_id": "bar", "name": "baz"}
With this, you would receive in the JSON Response:
{
"id": "foo",
"loan_id": "bar",
"name": "baz"
}
And the JSON Schema would have id
and loan_id
:
Thanks for providing the "How to do it" example. Can confirm that it works, but it does make the code more convoluted. It becomes even more convoluted when bringing SQL Alchemy and orm_mode into the mix. Building on the same example, I could not get something like this to work:
@app.get("/", response_model=TransactionResponse)
def read_root():
return db.query(TransactionModel).first()
One way I got it to work was like this:
@app.get("/", response_model=TransactionResponse)
def read_root():
transaction = db.query(TransactionModel).first()
return TransactionResponse(**Transaction.from_orm(transaction).dict())
Please let me know if there is a less messy way!
Here's a complete self-contained example:
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, Field
from fastapi import FastAPI, Depends
Base = declarative_base()
class TransactionModel(Base):
__tablename__ = "transactions"
_id = Column(Integer, primary_key=True)
loanId = Column(String)
name = Column(String)
class Transaction(BaseModel):
id: str = Field(..., alias="_id")
loan_id: str = Field(..., alias="loanId")
name: str # to show that you can reuse fields with inheritance
class Config:
orm_mode = True
allow_population_by_field_name = True
class TransactionResponse(Transaction):
id: str
loan_id: str
app = FastAPI()
@app.get("/", response_model=TransactionResponse)
def read_root():
transaction = TransactionModel(_id=1, loanId='ABC', name='Hello')
return transaction # <-- this does not work
# return TransactionResponse(**Transaction.from_orm(transaction).dict()) # <-- this works, but messy
(Btw. In my case, the issue is that I need to return a field called "metadata", but "metadata" is reserved on SQL Alchemy models, so I use the workaround described here)
wish I had found this thread sooner! Looks like there is a pretty good workaround above but just in case it's helpful to anyone, I was able to patch pydantic
so that it shows that it will accept the alias or the field
https://github.com/samuelcolvin/pydantic/pull/3005
huge danger
Here's a hack that changes default behaviour to generate docs by field names instead of aliases:
from functools import wraps
from unittest.mock import patch
from fastapi.utils import get_model_definitions
from pydantic.schema import model_process_schema
@wraps(model_process_schema)
def model_process_schema_wrapper(*args, **kwargs):
kwargs.setdefault("by_alias", False)
return model_process_schema(*args, **kwargs)
# Forces OpenAPI docs to have field names instead of aliases
# https://github.com/tiangolo/fastapi/issues/771
fastapi_schema_patch = patch(
f"{get_model_definitions.__module__}.{model_process_schema.__name__}",
model_process_schema_wrapper,
)
fastapi_schema_patch.start()
Is there another way around besides @Bobronium solution ?