Featured image of post pydantic V2 迁移遇到的json 类型兼容问题

pydantic V2 迁移遇到的json 类型兼容问题

记录三种json 转换方案

pydantic v2 的json 兼容性问题


背景介绍: 项目里面有很多v1 时代的写法,例如:envs: List[EnvModel] = Field([], sa_column=Column(JSON), description="环境变量列表"), 在v1 时代pydantic 会自动帮你做泛型转换 在 Pydantic v1 中,可能通过隐式逻辑将 List[T] 与 JSON 类型关联,但 v2 移除了这种隐式关联,导致直接使用 sa_column=Column(JSON) 时,SQLAlchemy 无法将 List[EnvModel] 与 JSON 类型正确绑定,从而会引发报错 <class 'list'> has no matching SQLAlchemy type。本质原因是SQLAlchemy 对字段类型的推断依赖于明确的 Python 类型与数据库类型的映射(如 str → VARCHAR,int → INTEGER)。但对于泛型容器类型(如 List[EnvModel]),SQLAlchemy 本身无法直接识别为某个数据库类型(如 JSON),必须显式指定类型(如 Column(JSON))。

现在介绍三种解决方法:

方案1 手动添加方法进行转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class EnvModel(SQLModel):
    name: str = Field(..., nullable=False)
    value: str = Field(..., nullable=False)

env_list_adapter = TypeAdapter(list[EnvModel])

class APP(SQLModel, table=True):
    __tablename__ = "apps"

    id: int | None = Field(default=None, primary_key=True)
    name: str = Field("应用名称", nullable=False, description="应用名称")
    envs_json: str = Field("",sa_column=Column(JSON),alias="envs")
    def __init__(self,**data:Any):
        envs_ = data.pop("envs",None)
        super().__init__(**data)
        if isinstance(envs_,list):
            self.envs = envs_
                    
    @property
    def envs(self)->list[EnvModel]:
        if isinstance(self.envs_json,str) and self.envs_json:
            try:
                return env_list_adapter.validate_json(self.envs_json)
            except Exception:
                return []
        return env_list_adapter.validate_python(self.envs_json)
    
    @envs.setter
    def envs(self,val:list[EnvModel]):
        self.envs_json = env_list_adapter.dump_json(val).decode()

优点: 简单易懂,简单类转换一下也不会出啥问题
缺点: 改动较多,一个字段最少得多两个方法和一个TypeAdapter,在大量使用这种复合类型字段的情况下,这种改动太多了

方案2 减少对类的改动,通过外部转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class EnvModel(SQLModel):
    name: str = Field(..., nullable=False)
    value: str = Field(..., nullable=False)
    class Config:
        orm_mode = True
        
class APP(SQLModel, table=True):
    __tablename__ = "apps"

    id: int | None = Field(default=None, primary_key=True)
    name: str = Field("应用名称", nullable=False, description="应用名称")
    # 这里用list[dict] 字典列表来做类型注解,自动转换为常见的array json类型
    envs: list[dict] = Field( sa_column=Column(ARRAY(JSON)),alias="envs") 

优点: 对类几乎无改动,用的也是基本类型字典
缺点: 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async with async_session() as session:
    try:
        test = APP(
            name="测试服务",
            envs=[
                    # 赋值需要转成dict 
                    EnvModel(name="PATH", value="/usr/local/bin").model_dump(),
                    EnvModel(name="PYTHONPATH", value="/app").model_dump(),
                    EnvModel(name="DEBUG", value="true").model_dump()
                ]
            )

        session.add(test)
        await session.commit()
        await session.refresh(test)

        logger.info(f"创建的服务 ID: {test.id}")
        logger.info(f"环境变量数量: {len(test.envs)}")

        result = await session.execute(select(APP).where(APP.id == test.id))
        db_app = result.first()

        logger.info(f"查询到的服务名称: {db_app.name}")
        logger.info("环境变量:")
        for env in db_app.envs:
            # 获取时需要用EnvModel 来检验类型,并转换成对应实例
            env = EnvModel(**env)
            logger.info(f"  {env.name}: {env.value}")

方案3 自定义类型适配器,推荐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
T = TypeVar('T', bound=SQLModel)


class SQLModelListType(TypeDecorator, Generic[T]):
    """
    自定义SQLAlchemy类型,自动处理:
    - 写入:list[SQLModel] -> list[dict](存储为JSON数组)
    - 读取:list[dict] -> list[SQLModel]
    """
    # 基础类型:PostgreSQL的ARRAY(JSON)或纯JSON
    impl = JSON  # 若用纯JSON则改为JSON

    def __init__(self, model_cls: Type[T], *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model_cls: Type[T] = model_cls  # 明确标注类型

    def process_bind_param(self, value: List[T], dialect) -> List[dict]:
        """写入数据库时:将模型列表转为字典列表"""
        if not value:
            return []
        return [item.model_dump() for item in value]

    def process_result_value(self, value: List[dict], dialect) -> List[T]:
        """从数据库读取时:将字典列表转为模型列表"""
        if not value:
            return []
        return [self.model_cls(** item) for item in value]


class EnvModel(SQLModel):
    name: str = Field(..., nullable=False)
    value: str = Field(..., nullable=False)



class APP(SQLModel, table=True):
    __tablename__ = "apps"

    id: int | None = Field(default=None, primary_key=True)
    name: str = Field("应用名称", nullable=False, description="应用名称")
    # 核心改动:使用自定义类型,指定模型类型为EnvModel
    envs: list[EnvModel] = Field(
        default_factory=list,
        sa_column=Column(SQLModelListType(EnvModel))  # 绑定自定义类型
    )

优点: 能复用这个类型装饰器,实现v2 的类型迁移,几乎无感,改动也仅限于对应字段的那行,完全可以接受这个工作量
缺点: 自定义sqlalchemy 类型装饰器的场景比较少见,用来做sa_column 可能没之前那么直白,理解难度会高一些

sqlalchemy 的类型装饰器

SQLAlchemy 的 TypeDecorator 是用于自定义数据库类型的核心工具,它通过封装底层数据库类型并覆盖特定方法,实现 Python 类型与数据库类型的双向转换。其核心方法如下:

  1. impl:指定底层数据库类型(必须定义)
    impl 是 TypeDecorator 的核心类属性,用于指定当前自定义类型基于哪个 SQLAlchemy 原生类型(如 Integer、String、JSON 等)。所有数据库交互最终会委托给这个底层类型。
    示例

    1
    2
    3
    4
    
    from sqlalchemy import TypeDecorator, JSON
    
    class MyJsonType(TypeDecorator):
        impl = JSON  # 基于原生 JSON 类型扩展
    
  2. process_bind_param(self, value, dialect):Python → 数据库(写入时)
    作用:将 Python 中的值(如自定义对象、复杂类型)转换为底层数据库类型可接受的格式(如 JSON 类型接受字典 / 列表,String 接受字符串)。
    参数:

    • value:Python 中要写入数据库的值(可能为 None)。
    • dialect:当前数据库方言(如 postgresql、mysql),可用于适配不同数据库的差异。
    • 返回值:转换后的值(需符合 impl 类型的要求)。

    示例

    1
    2
    3
    4
    5
    
    def process_bind_param(self, value, dialect):
        if value is None:
            return []
        # 将自定义模型列表转为字典列表(适配 JSON 类型)
        return [item.dict() for item in value]
    
  3. process_result_value(self, value, dialect):数据库 → Python(读取时)
    作用:将从数据库读取的值(如 JSON 解析后的字典)转换为 Python 中需要的类型(如自定义模型、复杂对象)。
    参数:

  • value:从数据库读取的值(可能为 None,格式由 impl 类型决定)。
  • dialect:当前数据库方言。
  • 返回值:转换后的 Python 对象(如自定义模型实例)。
    示例
    1
    2
    3
    4
    5
    6
    
    
    def process_result_value(self, value, dialect):
        if not value:
            return []
        # 将字典列表转为自定义模型列表
        return [MyModel(** item) for item in value]
    
  1. process_literal_param(self, value, dialect):处理 SQL 字面量(可选)
    作用:当值以字面量形式嵌入 SQL 语句(如 INSERT VALUES (?) 中的 ?)时,将其转换为符合 SQL 语法的字符串。
    默认行为:若未实现,会使用 impl 类型的 process_literal_param 方法。 通常用于自定义类型需要特殊 SQL 字面量格式的场景(如日期格式化)。
    示例
    1
    2
    3
    4
    5
    
    def process_literal_param(self, value, dialect):
        if value is None:
            return 'NULL'
        # 对字符串类型值添加单引号,避免 SQL 注入风险
        return f"'{value}'"
    
Licensed under CC BY-NC-SA 4.0
往日已经不在,未来尚未开始
使用 Hugo 构建
主题 StackJimmy 设计