11from typing import Any , cast
22
3+ from pydantic import BaseModel
34from sqlalchemy import types
45from sqlalchemy .engine .interfaces import Dialect
6+ from sqlalchemy .dialects .postgresql import JSONB # for Postgres JSONB
57
68
79class AutoString (types .TypeDecorator ): # type: ignore
@@ -14,3 +16,28 @@ def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]":
1416 if impl .length is None and dialect .name == "mysql" :
1517 return dialect .type_descriptor (types .String (self .mysql_default_length ))
1618 return super ().load_dialect_impl (dialect )
19+
20+
21+ class PydanticJSONB (types .TypeDecorator ): # type: ignore
22+ """Custom type to automatically handle Pydantic model serialization."""
23+
24+ impl = JSONB # use JSONB type in Postgres (fallback to JSON for others)
25+ cache_ok = True # allow SQLAlchemy to cache results
26+
27+ def __init__ (self , model_class , * args , ** kwargs ):
28+ super ().__init__ (* args , ** kwargs )
29+ self .model_class = model_class # Pydantic model class to use
30+
31+ def process_bind_param (self , value , dialect ):
32+ # Called when storing to DB: convert Pydantic model to a dict (JSON-serializable)
33+ if value is None :
34+ return None
35+ if isinstance (value , BaseModel ):
36+ return value .model_dump ()
37+ return value # assume it's already a dict
38+
39+ def process_result_value (self , value , dialect ):
40+ # Called when loading from DB: convert dict to Pydantic model instance
41+ if value is None :
42+ return None
43+ return self .model_class .parse_obj (value ) # instantiate Pydantic model
0 commit comments