voluptuous,一个非常实用的 Python 库!
线上接口最烦的一类报错,不是 500。
是那种字段传错了,但日志里只剩一句:
order submit failed: invalid payload
然后你顺着链路翻半天,发现前端把 amount 从数字传成了字符串,pay_type 多传了个空格,items 里面还有个商品数量是 0。
这种问题我一般不想在业务代码里一层层 if。
太脏。
Python 里有个库,叫 voluptuous,干的就是这类脏活:校验字典、配置、接口参数、JSON 数据。它不像 Pydantic 那么重,也不像手写判断那么散。小脚本、后台任务、内部接口,用起来很顺手。
安装就一行:
pip install voluptuous
先看一个接口参数。
from voluptuous import Schema, Required, Optional, All, Length, Range, In, Coerce, MultipleInvalidorder_schema = Schema({
Required("user_id"): All(str, Length(min=6, max=32)),
Required("amount"): All(Coerce(float), Range(min=0.01)),
Required("pay_type"): In(["wechat", "alipay", "balance"]),
Optional("remark", default=""): All(str, Length(max=120)),
})
这里我最喜欢的是 Coerce。
实际接口里你很少能保证上游传来的类型完全干净。尤其是管理后台、Excel 导入、老系统对接,金额字段今天是 12.8,明天可能就是 "12.8"。
不用 Coerce 的话,代码大概率会写成这样:
amount = data.get("amount")
try:
amount = float(amount)
except Exception:
raise ValueError("amount error")if amount <= 0:
raise ValueError("amount error")
这种代码写多了,业务函数就开始发臭。校验、转换、业务判断混在一起,后面排查问题很难受。
用 voluptuous 可以把入口先收干净:
defsubmit_order(raw):
try:
data = order_schema(raw)
except MultipleInvalid as e:
print(f"bad order payload: {e}")
return {"ok": False, "msg": str(e)}# 走到这里,amount 已经是 float,remark 也有默认值
print("create order:", data)
return {"ok": True}
试一下:
submit_order({
"user_id": "u10086",
"amount": "19.90",
"pay_type": "wechat"
})
拿到的数据会变成:
{
"user_id": "u10086",
"amount": 19.9,
"pay_type": "wechat",
"remark": ""
}
这地方很适合放在接口入口、消息消费入口、批量导入入口。
我见过不少 Python 项目,参数校验全靠业务函数里面随手判断。刚开始没问题,字段一多就开始乱。尤其是 MQ 消费,消息一旦有脏数据,消费失败重试,重试失败又进死信,最后查半天,发现只是一个字段少了。
这种地方就应该先把消息拦下来。
from voluptuous import Schema, Required, Optional, All, Coerce, Range, Instock_msg_schema = Schema({
Required("sku"): All(str, Length(min=3, max=40)),
Required("warehouse"): In(["bj", "sh", "gz"]),
Required("change"): All(Coerce(int), Range(min=-9999, max=9999)),
Optional("trace_id", default="-"): str,
})
消费时不要上来就改库存:
defhandle_stock_message(msg):
try:
event = stock_msg_schema(msg)
except MultipleInvalid as e:
print(f"drop dirty stock msg, err={e}, raw={msg}")
returnif event["change"] == 0:
print(f"ignore empty stock change, trace={event['trace_id']}")
return
print(
f"apply stock, sku={event['sku']}, "
f"wh={event['warehouse']}, change={event['change']}, "
f"trace={event['trace_id']}"
)
我这里会直接把脏消息丢掉,还是重试,要看业务。
库存、资金、订单状态这类消息,不能随便丢,最好落异常表。日志、埋点、同步缓存这种消息,字段错了还无限重试,就有点自己折磨自己了。
voluptuous 还有个很实用的点,能校验嵌套结构。
比如一个订单里有商品列表:
item_schema = Schema({
Required("sku"): All(str, Length(min=3, max=40)),
Required("qty"): All(Coerce(int), Range(min=1, max=999)),
Optional("gift", default=False): bool,
})checkout_schema = Schema({
Required("buyer"): All(str, Length(min=6, max=32)),
Required("items"): All([item_schema], Length(min=1)),
Optional("coupon"): str,
})
这个写法比自己循环判断清爽很多。
defcheckout(raw):
try:
data = checkout_schema(raw)
except MultipleInvalid as e:
print(f"checkout payload invalid: {e}")
return total_qty = sum(x["qty"] for x in data["items"])
print(f"buyer={data['buyer']}, total_qty={total_qty}")
注意这里的 [item_schema],表示列表里的每个元素都要符合 item_schema。
这东西看着简单,但批量导入特别有用。
比如运营给你一个 JSON 文件,里面几百条商品配置。你要是不先校验,导到一半才发现某条数据少字段,那回滚、补偿、重跑都麻烦。
我一般会先写一个小校验脚本:
import json
from pathlib import Path
from voluptuous import Schema, Required, Optional, All, Coerce, Range, Length, MultipleInvalidproduct_schema = Schema({
Required("sku"): All(str, Length(min=3, max=40)),
Required("name"): All(str, Length(min=1, max=80)),
Required("price"): All(Coerce(float), Range(min=0)),
Optional("online", default=False): bool,
})
defcheck_file(path):
rows = json.loads(Path(path).read_text(encoding="utf-8"))
bad = []
for idx, row in enumerate(rows, 1):
try:
product_schema(row)
except MultipleInvalid as e:
bad.append((idx, str(e), row.get("sku")))
if bad:
for idx, err, sku in bad[:20]:
print(f"line={idx}, sku={sku}, err={err}")
raise SystemExit(f"found dirty rows: {len(bad)}")
print(f"all clean, rows={len(rows)}")
这个脚本不复杂,但很值。
因为它把问题拦在导入前,而不是让问题进数据库以后再补锅。
还有配置文件也适合用它。
Python 项目里经常会有这种配置:
raw_config = {
"redis": {
"host": "127.0.0.1",
"port": "6379",
"db": "0"
},
"worker": {
"threads": "8",
"mode": "fast"
}
}
端口、线程数这种字段,不校验也能跑。直到某天配置写成 "8c",程序启动到一半才炸。
config_schema = Schema({
Required("redis"): {
Required("host"): str,
Required("port"): All(Coerce(int), Range(min=1, max=65535)),
Optional("db", default=0): All(Coerce(int), Range(min=0, max=15)),
},
Required("worker"): {
Optional("threads", default=4): All(Coerce(int), Range(min=1, max=64)),
Optional("mode", default="safe"): In(["safe", "fast"]),
}
})
启动时先跑一下:
defload_config(raw):
try:
return config_schema(raw)
except MultipleInvalid as e:
raise RuntimeError(f"bad app config: {e}") from e
配置错了,启动阶段就死。
我更喜欢这种死法。比程序跑起来以后,某个后台线程半夜悄悄挂掉强多了。
不过 voluptuous 也不是万能的。
它适合轻量校验,适合字典结构,适合脚本和服务入口。你要是做大型 API,字段很多,还要自动生成文档、IDE 类型提示、复杂模型转换,那 Pydantic 更合适。
但很多内部工具根本不需要那么重。
一个定时脚本,一个数据清洗任务,一个 MQ 消费者,一个后台接口,把入口数据校验干净,就能少掉一堆低级脏问题。
我对这类库的判断很简单:它不应该让业务代码变复杂。
voluptuous 这个库好的地方就在这。校验规则单独放,业务函数拿到的就是相对干净的数据。出错时也能把错误卡在入口,不让脏数据继续往后流。
写 Python 小工具时,我一般会把它放在 schema.py 里。
业务代码不关心字段怎么来的,只关心字段已经对了。
这就够了。