fastapi / sqlmodel

SQL databases in Python, designed for simplicity, compatibility, and robustness.
https://sqlmodel.tiangolo.com/
MIT License
14.69k stars 670 forks source link

There seems to be a bug in using model with aliased fields to define request body #374

Open boh5 opened 2 years ago

boh5 commented 2 years ago

First Check

Commit to Help

Example Code

from typing import Optional

import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from sqlmodel import Field, Session, SQLModel, create_engine, select

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str = Field(alias='secretName')
    age: Optional[int] = Field(default=None, index=True)

class HeroPydantic(BaseModel):
    id: Optional[int] = Field(default=None)
    name: str = Field()
    secret_name: str = Field(alias='secretName')
    age: Optional[int] = Field(default=None)

sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)

app = FastAPI()

@app.post("/heroes/")
def create_hero(hero: Hero):
    with Session(engine) as session:
        session.add(hero)
        session.commit()
        session.refresh(hero)
        return hero

if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8081)

Description

  1. Create Hero model, the secret_name field aliased to secretName
  2. Post data
    {
    "name": "string",
    "secretName": "string",
    "age": 0
    }
  3. Then get hero.secret_name is None
  4. If I replace hero: Hero to hero: HeroPydantic, the hero.secret_name can get correct value from post data. But the strainge thing is that I can get correct hero.secret_name by hero: Hero = Hero.parse_obj(hero.dict(by_alias=True))

So it seems that alias argument in sqlmodel seems not working for converting body from post data, I have to write two models in order to achieve that.

Operating System

Windows

Operating System Details

No response

SQLModel Version

0.0.6

Python Version

3.7.6

Additional Context

No response

boh5 commented 2 years ago

I found that:

  1. Fastapi generate hero object from request body by function fastapi.dependencies.utils.request_body_to_args, and actually generate hero by this line v_, errors_ = field.validate(value, values, loc=loc)
  2. Then call sqlmodel.SQLModel.validate. In this function, transform alias dict value to field name dict values by values, fields_set, validation_error = validate_model(cls, value), then init model by model = cls(**values)
  3. But I did not set Hero model allow_population_by_field_name = True, so model = cls(**values) can not init correctlly
  4. I have tried set allow_population_by_field_name = True on Hero model, then it works, hero object contains the filed secret_name with value string
  5. If I replace hero: Hero to hero: HeroPydantic, v_, errors_ = field.validate(value, values, loc=loc) can init correctly. But I can't step into pydantic.ModelField.validate in debugger of pycharm-2022.1.3. So I can't find the reason why pydantic and sqlmodel have different results
Undertone0809 commented 10 months ago

@boh5 Do you have solution about this problem? I use the latest version and I still have this problem.

anthony2261 commented 9 months ago

Facing the same issue here

chenweiss commented 9 months ago

Would be amazing to get this addressed, if possible. It's blocking us from adopting this project.

riziles commented 9 months ago

Looks like there is an open PR to fix this: https://github.com/tiangolo/sqlmodel/pull/774#issuecomment-1883904625

coneillpj commented 8 months ago

Looks like there is an open PR to fix this: #774 (comment)

I think this PR will only partially fix this issue. It will allow a workaround using validation_alias but does not address the issue that alias keyword does not behave the same way as the Pydantic Field alias when it comes to validating models.

Do we know if this issue is on the radar to be addressed?

sergio-alegria commented 7 months ago

Facing a similar issue here, the alias is not taken into account when generating the body for the request.

I am also using pydantic's v2 alias_generator, and it works if alias is not defined in the field. If the alias is passed, the body doesn't apply the alias_generator and ignores aliases completely.

zoonman commented 4 months ago

I tried to work around the same issue but I ended with a similar bug. validation_alias works to set the field and store the data, but retrieval is not working properly. serialization_alias calls the default_factory.

E.g.:

class MyModel(SQLModel):
    since: datetime = Field(
        description="Begin of the interval",
        default_factory=date_factory(),
        sa_type=DateTime,
        index=True,
        alias="from",
        title="from",
        schema_extra={"validation_alias": "from",  "serialization_alias": "from"}
    )

SQLModel is supposed to expand pydantic contract instead of superseding it.

I hope for the prompt resolution because this bug prevents overcoming Python language limitations and limits API expression capabilities.

coolmian commented 3 months ago

Minimum Error Example:

from sqlmodel import SQLModel, Field
from typing import Optional

class SimpleBookChapter(SQLModel):
    class Config:
        populate_by_name = True

    chapter_number: int = Field(alias="episode_number")
    title: Optional[str] = Field(alias="episode_title")

chapter_data = {"episode_number": 1, "episode_title": "Test Chapter"}
chapter = SimpleBookChapter.model_validate(chapter_data)
print(chapter)

error log:

    chapter = SimpleBookChapter.model_validate(chapter_data)
  File "D:\ProgramData\Anaconda3\envs\chapchat\lib\site-packages\sqlmodel\main.py", line 848, in model_validate
    return sqlmodel_validate(
  File "D:\ProgramData\Anaconda3\envs\chapchat\lib\site-packages\sqlmodel\_compat.py", line 314, in sqlmodel_validate
    cls.__pydantic_validator__.validate_python(
pydantic_core._pydantic_core.ValidationError: 2 validation errors for SimpleBookChapter
chapter_number
  Field required [type=missing, input_value={'episode_number': 1, 'ep..._title': 'Test Chapter'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.8/v/missing
title
  Field required [type=missing, input_value={'episode_number': 1, 'ep..._title': 'Test Chapter'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.8/v/missing

success example:


from pydantic import Field
from sqlmodel import SQLModel
from typing import Optional

class SimpleBookChapter(SQLModel):
    class Config:
        populate_by_name = True

    chapter_number: int = Field(alias="episode_number")
    title: Optional[str] = Field(alias="episode_title")

chapter_data = {"episode_number": 1, "episode_title": "Test Chapter"}
chapter = SimpleBookChapter.model_validate(chapter_data)
print(chapter)

success log:

chapter_number=1 title='Test Chapter'
Meepoljdx commented 3 weeks ago
from pydantic import Field

It's work for me :)

dadodimauro commented 3 weeks ago

I was facing a similar problem where I wanted to have camel case fields for the endpoints, but snake case for the code and the database column. I solved overriding the Config class.

from sqlmodel import Field, SQLModel
from humps import camelize

def to_camel(string):
    return camelize(string)

class MyModel(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    my_field : str

    class Config:   # type: ignore
        populate_by_name = True
        alias_generator = to_camel

It worked but it remains the problem with the typechecking ("Config" overrides symbol of same name in class "SQLModel")