fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

[BUG] Schema is generated using field alias

Open aleksarias opened this issue 5 years ago • 9 comments

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:

  1. 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"}
  1. Open the browser and call the endpoint /docs.
  2. 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 avatar Dec 05 '19 16:12 aleksarias

@aleksarias , I cannot reproduce your error. What is the "ObjectIdStr" ?

rappongy avatar Jan 23 '20 12:01 rappongy

@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!

iishyfishyy avatar Feb 27 '20 05:02 iishyfishyy

Anyone found a workaround for this?

JinKazuya avatar Apr 08 '20 14:04 JinKazuya

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.

JinKazuya avatar Apr 09 '20 09:04 JinKazuya

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:

Selection_428

tiangolo avatar May 10 '21 09:05 tiangolo

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)

danmichaelo avatar May 16 '21 18:05 danmichaelo

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

jpribyl avatar Aug 21 '21 01:08 jpribyl

huge danger

linpan avatar Aug 02 '22 02:08 linpan

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

Bobronium avatar Jan 02 '23 17:01 Bobronium

Is there another way around besides @Bobronium solution ?

Raphencoder avatar Jan 18 '23 11:01 Raphencoder