Skip to content

Commit 0d1f9c0

Browse files
committed
✨ Automatically map dictionaries to JSON
1 parent a85de91 commit 0d1f9c0

File tree

3 files changed

+84
-10
lines changed

3 files changed

+84
-10
lines changed

sqlmodel/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
from sqlalchemy.orm.decl_api import DeclarativeMeta
5454
from sqlalchemy.orm.instrumentation import is_instrumented
5555
from sqlalchemy.sql.schema import MetaData
56-
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
56+
from sqlalchemy.sql.sqltypes import JSON, LargeBinary, Time, Uuid
5757
from typing_extensions import Literal, TypeAlias, deprecated, get_origin
5858

5959
from ._compat import ( # type: ignore[attr-defined]
@@ -700,6 +700,8 @@ def get_sqlalchemy_type(field: Any) -> Any:
700700
)
701701
if issubclass(type_, uuid.UUID):
702702
return Uuid
703+
if issubclass(type_, dict):
704+
return JSON
703705
raise ValueError(f"{type_} has no matching SQLAlchemy type")
704706

705707

tests/test_dict.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from typing import Any, Dict, Optional
2+
3+
from sqlmodel import Field, Session, SQLModel, create_engine
4+
from typing_extensions import TypedDict
5+
6+
7+
def test_dict_maps_to_json(clear_sqlmodel):
8+
class Resource(SQLModel, table=True):
9+
id: Optional[int] = Field(default=None, primary_key=True)
10+
name: str
11+
data: dict[str, Any]
12+
13+
engine = create_engine("sqlite://")
14+
SQLModel.metadata.create_all(engine)
15+
16+
resource = Resource(name="test", data={"key": "value", "num": 42})
17+
18+
with Session(engine) as session:
19+
session.add(resource)
20+
session.commit()
21+
session.refresh(resource)
22+
23+
assert resource.data["key"] == "value"
24+
assert resource.data["num"] == 42
25+
26+
27+
def test_typing_dict_maps_to_json(clear_sqlmodel):
28+
"""Test if typing.Dict type annotation works without explicit sa_type"""
29+
30+
class Resource(SQLModel, table=True):
31+
id: Optional[int] = Field(default=None, primary_key=True)
32+
name: str
33+
data: Dict[str, int]
34+
35+
engine = create_engine("sqlite://")
36+
SQLModel.metadata.create_all(engine)
37+
38+
resource = Resource(name="test", data={"count": 100})
39+
40+
with Session(engine) as session:
41+
session.add(resource)
42+
session.commit()
43+
session.refresh(resource)
44+
45+
assert resource.data["count"] == 100
46+
47+
48+
class Metadata(TypedDict):
49+
name: str
50+
email: str
51+
52+
53+
def test_typeddict_automatic_json_mapping(clear_sqlmodel):
54+
"""
55+
Test that TypedDict fields automatically map to JSON type.
56+
57+
This fixes the original error:
58+
ValueError: <class 'app.models.NeonMetadata'> has no matching SQLAlchemy type
59+
"""
60+
61+
class ConnectedResource(SQLModel, table=True):
62+
id: Optional[int] = Field(default=None, primary_key=True)
63+
name: str
64+
neon_metadata: Metadata
65+
66+
engine = create_engine("sqlite://")
67+
SQLModel.metadata.create_all(engine)
68+
69+
resource = ConnectedResource(
70+
name="my-resource",
71+
neon_metadata={"name": "John Doe", "email": "[email protected]"},
72+
)
73+
74+
with Session(engine) as session:
75+
session.add(resource)
76+
session.commit()
77+
session.refresh(resource)
78+
79+
assert resource.neon_metadata["name"] == "John Doe"
80+
assert resource.neon_metadata["email"] == "[email protected]"

tests/test_sqlalchemy_type_errors.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List, Optional, Union
1+
from typing import List, Optional, Union
22

33
import pytest
44
from sqlmodel import Field, SQLModel
@@ -12,14 +12,6 @@ class Hero(SQLModel, table=True):
1212
tags: List[str]
1313

1414

15-
def test_type_dict_breaks() -> None:
16-
with pytest.raises(ValueError):
17-
18-
class Hero(SQLModel, table=True):
19-
id: Optional[int] = Field(default=None, primary_key=True)
20-
tags: Dict[str, Any]
21-
22-
2315
def test_type_union_breaks() -> None:
2416
with pytest.raises(ValueError):
2517

0 commit comments

Comments
 (0)