Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
8 tasks done
boh5 opened this issue Jul 12, 2022 · 11 comments
Open
8 tasks done
Labels
question Further information is requested

Comments

@boh5
Copy link

boh5 commented Jul 12, 2022

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the SQLModel documentation, with the integrated search.
  • I already searched in Google "How to X in SQLModel" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to SQLModel but to Pydantic.
  • I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • I commit to help with one of those options 👆

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
}
  1. Then get hero.secret_name is None
  2. 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 boh5 added the question Further information is requested label Jul 12, 2022
@boh5
Copy link
Author

boh5 commented Jul 17, 2022

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
Copy link

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

@anthony2261
Copy link

Facing the same issue here

@chenweiss
Copy link

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

@riziles
Copy link

riziles commented Feb 21, 2024

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

@coneillpj
Copy link

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
Copy link

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
Copy link

zoonman commented Jul 29, 2024

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
Copy link

coolmian commented Aug 26, 2024

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
Copy link

from pydantic import Field

It's work for me :)

@dadodimauro
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests