Skip to content

Commit fa2f178

Browse files
tiangolofarahats9sbor23peterlandryAntonDeMeester
authored
✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available), including initial work by AntonDeMeester (#722)
Co-authored-by: Mohamed Farahat <[email protected]> Co-authored-by: Stefan Borer <[email protected]> Co-authored-by: Peter Landry <[email protected]> Co-authored-by: Anton De Meester <[email protected]>
1 parent 5b733b3 commit fa2f178

File tree

79 files changed

+2617
-520
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2617
-520
lines changed

.github/workflows/test.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
- "3.10"
2828
- "3.11"
2929
- "3.12"
30+
pydantic-version:
31+
- pydantic-v1
32+
- pydantic-v2
3033
fail-fast: false
3134

3235
steps:
@@ -57,9 +60,15 @@ jobs:
5760
- name: Install Dependencies
5861
if: steps.cache.outputs.cache-hit != 'true'
5962
run: python -m poetry install
63+
- name: Install Pydantic v1
64+
if: matrix.pydantic-version == 'pydantic-v1'
65+
run: pip install "pydantic>=1.10.0,<2.0.0"
66+
- name: Install Pydantic v2
67+
if: matrix.pydantic-version == 'pydantic-v2'
68+
run: pip install "pydantic>=2.0.2,<3.0.0"
6069
- name: Lint
6170
# Do not run on Python 3.7 as mypy behaves differently
62-
if: matrix.python-version != '3.7'
71+
if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2'
6372
run: python -m poetry run bash scripts/lint.sh
6473
- run: mkdir coverage
6574
- name: Test

docs/tutorial/fastapi/multiple-models.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,17 @@ Now we use the type annotation `HeroCreate` for the request JSON data in the `he
175175
# Code below omitted 👇
176176
```
177177

178-
Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.from_orm()`.
178+
Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.model_validate()`.
179179

180-
The method `.from_orm()` reads data from another object with attributes and creates a new instance of this class, in this case `Hero`.
180+
The method `.model_validate()` reads data from another object with attributes (or a dict) and creates a new instance of this class, in this case `Hero`.
181181

182-
The alternative is `Hero.parse_obj()` that reads data from a dictionary.
182+
In this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.model_validate()` to read those attributes.
183183

184-
But as in this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.from_orm()` to read those attributes.
184+
/// tip
185+
In versions of **SQLModel** before `0.0.14` you would use the method `.from_orm()`, but it is now deprecated and you should use `.model_validate()` instead.
186+
///
185187

186-
With this, we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
188+
We can now create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
187189

188190
```Python hl_lines="3"
189191
# Code above omitted 👆

docs/tutorial/fastapi/update.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ So, we need to read the hero from the database, with the **same logic** we used
9090

9191
The `HeroUpdate` model has all the fields with **default values**, because they all have defaults, they are all optional, which is what we want.
9292

93-
But that also means that if we just call `hero.dict()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example:
93+
But that also means that if we just call `hero.model_dump()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example:
9494

9595
```Python
9696
{
@@ -102,7 +102,7 @@ But that also means that if we just call `hero.dict()` we will get a dictionary
102102

103103
And then, if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**.
104104

105-
But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`.
105+
But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.model_dump()` method for that: `exclude_unset=True`.
106106

107107
This tells Pydantic to **not include** the values that were **not sent** by the client. Saying it another way, it would **only** include the values that were **sent by the client**.
108108

@@ -112,7 +112,7 @@ So, if the client sent a JSON with no values:
112112
{}
113113
```
114114

115-
Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be:
115+
Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be:
116116

117117
```Python
118118
{}
@@ -126,7 +126,7 @@ But if the client sent a JSON with:
126126
}
127127
```
128128

129-
Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be:
129+
Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be:
130130

131131
```Python
132132
{
@@ -152,6 +152,9 @@ Then we use that to get the data that was actually sent by the client:
152152

153153
///
154154

155+
/// tip
156+
Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2.
157+
155158
## Update the Hero in the Database
156159

157160
Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`.
@@ -208,7 +211,7 @@ So, if the client wanted to intentionally remove the `age` of a hero, they could
208211
}
209212
```
210213

211-
And when getting the data with `hero.dict(exclude_unset=True)`, we would get:
214+
And when getting the data with `hero.model_dump(exclude_unset=True)`, we would get:
212215

213216
```Python
214217
{
@@ -226,4 +229,4 @@ These are some of the advantages of Pydantic, that we can use with SQLModel.
226229

227230
## Recap
228231

229-
Using `.dict(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎
232+
Using `.model_dump(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎

docs/tutorial/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ There's a chance that you have multiple Python versions installed.
8282

8383
You might want to try with the specific versions, for example with:
8484

85-
* `python3.11`
8685
* `python3.12`
86+
* `python3.11`
8787
* `python3.10`
8888
* `python3.9`
8989

docs_src/tutorial/fastapi/app_testing/tutorial001/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def on_startup():
5454

5555
@app.post("/heroes/", response_model=HeroRead)
5656
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
57-
db_hero = Hero.from_orm(hero)
57+
db_hero = Hero.model_validate(hero)
5858
session.add(db_hero)
5959
session.commit()
6060
session.refresh(db_hero)
@@ -87,7 +87,7 @@ def update_hero(
8787
db_hero = session.get(Hero, hero_id)
8888
if not db_hero:
8989
raise HTTPException(status_code=404, detail="Hero not found")
90-
hero_data = hero.dict(exclude_unset=True)
90+
hero_data = hero.model_dump(exclude_unset=True)
9191
for key, value in hero_data.items():
9292
setattr(db_hero, key, value)
9393
session.add(db_hero)

docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def on_startup():
5252

5353
@app.post("/heroes/", response_model=HeroRead)
5454
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
55-
db_hero = Hero.from_orm(hero)
55+
db_hero = Hero.model_validate(hero)
5656
session.add(db_hero)
5757
session.commit()
5858
session.refresh(db_hero)
@@ -85,7 +85,7 @@ def update_hero(
8585
db_hero = session.get(Hero, hero_id)
8686
if not db_hero:
8787
raise HTTPException(status_code=404, detail="Hero not found")
88-
hero_data = hero.dict(exclude_unset=True)
88+
hero_data = hero.model_dump(exclude_unset=True)
8989
for key, value in hero_data.items():
9090
setattr(db_hero, key, value)
9191
session.add(db_hero)

docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def on_startup():
5454

5555
@app.post("/heroes/", response_model=HeroRead)
5656
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
57-
db_hero = Hero.from_orm(hero)
57+
db_hero = Hero.model_validate(hero)
5858
session.add(db_hero)
5959
session.commit()
6060
session.refresh(db_hero)
@@ -87,7 +87,7 @@ def update_hero(
8787
db_hero = session.get(Hero, hero_id)
8888
if not db_hero:
8989
raise HTTPException(status_code=404, detail="Hero not found")
90-
hero_data = hero.dict(exclude_unset=True)
90+
hero_data = hero.model_dump(exclude_unset=True)
9191
for key, value in hero_data.items():
9292
setattr(db_hero, key, value)
9393
session.add(db_hero)

docs_src/tutorial/fastapi/delete/tutorial001.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def on_startup():
5050
@app.post("/heroes/", response_model=HeroRead)
5151
def create_hero(hero: HeroCreate):
5252
with Session(engine) as session:
53-
db_hero = Hero.from_orm(hero)
53+
db_hero = Hero.model_validate(hero)
5454
session.add(db_hero)
5555
session.commit()
5656
session.refresh(db_hero)
@@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
7979
db_hero = session.get(Hero, hero_id)
8080
if not db_hero:
8181
raise HTTPException(status_code=404, detail="Hero not found")
82-
hero_data = hero.dict(exclude_unset=True)
82+
hero_data = hero.model_dump(exclude_unset=True)
8383
for key, value in hero_data.items():
8484
setattr(db_hero, key, value)
8585
session.add(db_hero)

docs_src/tutorial/fastapi/delete/tutorial001_py310.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def on_startup():
4848
@app.post("/heroes/", response_model=HeroRead)
4949
def create_hero(hero: HeroCreate):
5050
with Session(engine) as session:
51-
db_hero = Hero.from_orm(hero)
51+
db_hero = Hero.model_validate(hero)
5252
session.add(db_hero)
5353
session.commit()
5454
session.refresh(db_hero)
@@ -77,7 +77,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
7777
db_hero = session.get(Hero, hero_id)
7878
if not db_hero:
7979
raise HTTPException(status_code=404, detail="Hero not found")
80-
hero_data = hero.dict(exclude_unset=True)
80+
hero_data = hero.model_dump(exclude_unset=True)
8181
for key, value in hero_data.items():
8282
setattr(db_hero, key, value)
8383
session.add(db_hero)

docs_src/tutorial/fastapi/delete/tutorial001_py39.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def on_startup():
5050
@app.post("/heroes/", response_model=HeroRead)
5151
def create_hero(hero: HeroCreate):
5252
with Session(engine) as session:
53-
db_hero = Hero.from_orm(hero)
53+
db_hero = Hero.model_validate(hero)
5454
session.add(db_hero)
5555
session.commit()
5656
session.refresh(db_hero)
@@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
7979
db_hero = session.get(Hero, hero_id)
8080
if not db_hero:
8181
raise HTTPException(status_code=404, detail="Hero not found")
82-
hero_data = hero.dict(exclude_unset=True)
82+
hero_data = hero.model_dump(exclude_unset=True)
8383
for key, value in hero_data.items():
8484
setattr(db_hero, key, value)
8585
session.add(db_hero)

0 commit comments

Comments
 (0)