如何在SQLModel中定义带约束的Optional[Decimal]字段并生成合规JSON Schema
在SQLModel中为可选Decimal字段保留约束并实现字段级自定义正则
我在SQLModel里定义基类和表模型时,碰到个麻烦事:要把Decimal字段设为可选,同时得保住所有约束——数据库层面的精度限制、Pydantic的数值校验、还有JSON Schema里的规则,试了好几种方法都有坑:
- 直接在Field里加
max_digits和decimal_places,会炸出ValueError: Unknown constraint max_digits - 照搬Pydantic的Annotated方案,在带
table=True的模型里会触发TypeError: issubclass() arg 1 must be a class - 用
sa_column指定数据库约束的临时办法,虽然Pydantic的ge/le校验能正常工作,但生成的JSON Schema丢了所有约束,导致用jsf生成测试假数据全失败
后来拆分基类和表模型的字段定义,解决了约束保留的问题,但自定义JSON Schema生成器时,所有Decimal字段会共用同一个正则,没法给单个字段指定专属规则。下面是解决这个问题的具体方案:
1. 字段定义时通过metadata传递自定义正则
在基类的Annotated Field里,用metadata参数给每个Decimal字段存专属的正则表达式,表模型里单独配置数据库层面的DECIMAL约束:
from sqlmodel import SQLModel, Field, Column, DECIMAL from decimal import Decimal from typing import Optional, Annotated from pydantic import core_schema, GenerateJsonSchema, JsonSchemaValue class ItemBase(SQLModel): # 给price字段指定最多4位小数的正则 price: Optional[Annotated[Decimal, Field( default=None, multiple_of=0.0001, ge=0, le=100, metadata={"decimal_regex": r"^\d+(\.\d{1,4})?$"} )]] = None # 给tax字段指定最多2位小数的正则,和price区分开 tax: Optional[Annotated[Decimal, Field( default=None, multiple_of=0.01, ge=0, le=10, metadata={"decimal_regex": r"^\d+(\.\d{1,2})?$"} )]] = None class ItemTable(ItemBase, table=True): __tablename__ = "item" id: Optional[int] = Field(default=None, primary_key=True) # 表模型里单独配置数据库层面的精度约束 price: Optional[Decimal] = Field( default=None, ge=0, le=100, sa_column=Column(DECIMAL(7,4)) ) tax: Optional[Decimal] = Field( default=None, ge=0, le=10, sa_column=Column(DECIMAL(5,2)) )
2. 修改Schema生成器,读取字段专属正则
自定义GenerateJsonSchema的子类,在处理Decimal字段时,从schema的metadata里取出对应的正则,实现字段级的差异化配置:
class CustomDecimalSchemaGenerator(GenerateJsonSchema): def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue: # 先拿到默认的number类型schema(带ge/le/multiple_of约束) number_schema = super().decimal_schema(schema) # 读取当前字段的自定义正则,没指定就用默认规则 custom_regex = schema.metadata.get( "decimal_regex", r"^[-+]?\d+((?:\.\d+)|(?:[eE][-+]?\d+))?$" ) # 构建带正则的string类型schema string_schema = self.str_schema(core_schema.str_schema(pattern=custom_regex)) # 合并number、string、null三种选项(因为字段是Optional) return { "anyOf": [number_schema, string_schema, {"type": "null"}], "default": schema.default, "title": schema.title }
3. 生成带字段级正则的JSON Schema
调用模型的model_json_schema方法时传入自定义生成器,就能得到每个Decimal字段带专属正则的Schema:
schema = ItemBase.model_json_schema(schema_generator=CustomDecimalSchemaGenerator) # 查看price字段的Schema(带4位小数的正则) print(schema["properties"]["price"]) # 输出: # {'anyOf': [{'maximum': 100.0, 'minimum': 0.0, 'multipleOf': 0.0001, 'type': 'number'}, {'pattern': '^\\d+(\\.\\d{1,4})?$', 'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Price'} # 查看tax字段的Schema(带2位小数的正则) print(schema["properties"]["tax"]) # 输出: # {'anyOf': [{'maximum': 10.0, 'minimum': 0.0, 'multipleOf': 0.01, 'type': 'number'}, {'pattern': '^\\d+(\\.\\d{1,2})?$', 'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Tax'}
原理说明
- 基类用
Annotated[Decimal, Field(...)]保留Pydantic的数值校验规则,同时通过metadata存储字段专属的正则; - 表模型重写字段,用
sa_column=Column(DECIMAL(...))指定数据库层面的精度约束,避免SQLModel混淆Pydantic和SQLAlchemy的参数; - 自定义Schema生成器通过读取
schema.metadata里的正则,为每个Decimal字段生成专属的string类型约束,解决了全局正则的问题。
内容的提问来源于stack exchange,提问作者Joachim Huet
相关产品推荐
相关产品推荐

