voluptuous,一个非常实用的 Python 库!
不是不会写校验,是真写多了以后,越来越烦那种“看着像 dict,跑起来全靠赌”的配置。
接口参数一层套一层,JSON 从前端进来,字段可能缺,类型可能飘,枚举值可能写错,最烦的是有些值单看格式没问题,组合起来才有问题。很多人这时候就上手写 if/else,写着写着,业务还没复杂,校验代码先烂了。
这类活我一般不太想手搓。不是不能写,是太碎,后面一改需求,校验逻辑比业务本身还难看。voluptuous 这种库,刚好就是拿来收拾这种场面的。
它不是那种看完文档觉得“理念先进”的库,它的好处很直接:把原本散在代码里的判断,收回到一份可读的 schema 里。字段要不要、类型对不对、值能不能为空、范围合不合理,放一起看,脑子会轻松很多。
先看个常见场景。比如一个任务调度接口,前端会丢这样一段数据过来:
payload = {
"task_name": "sync_user_profile",
"retry": 3,
"timeout": 15,
"notify_emails": ["[email protected]", "[email protected]"],
"enabled": True
}
不用 voluptuous 的写法,现场一般长这样:
defvalidate_job(data: dict):
if"task_name"notin data ornot isinstance(data["task_name"], str):
raise ValueError("task_name 非法")if"retry"in data andnot isinstance(data["retry"], int):
raise ValueError("retry 必须是 int")
if"timeout"notin data:
raise ValueError("timeout 不能为空")
ifnot isinstance(data["timeout"], int) or data["timeout"] <= 0:
raise ValueError("timeout 必须大于 0")
if"notify_emails"in data:
ifnot isinstance(data["notify_emails"], list):
raise ValueError("notify_emails 必须是 list")
for mail in data["notify_emails"]:
if"@"notin mail:
raise ValueError(f"邮箱格式不对: {mail}")
return data
这段不算长吧,但已经开始别扭了。字段再加几个,嵌套再深一点,就会进入“改一处炸三处”的状态。
换成 voluptuous,会顺很多:
from voluptuous import Schema, Required, Optional, All, Length, Range, Emailjob_schema = Schema({
Required("task_name"): All(str, Length(min=3, max=64)),
Optional("retry", default=0): All(int, Range(min=0, max=10)),
Required("timeout"): All(int, Range(min=1, max=300)),
Optional("notify_emails", default=[]): [Email()],
Optional("enabled", default=True): bool,
}, required=True)
payload = {
"task_name": "sync_user_profile",
"retry": 3,
"timeout": 15,
"notify_emails": ["[email protected]", "[email protected]"]
}
print(job_schema(payload))
这地方我第一眼就舒服多了。不是因为它“高级”,而是因为它把判断条件都摊平了。你不用翻业务代码,一眼就知道这个入参允许什么、不允许什么。
而且它不只是查类型,还能做组合校验。这个点在线上接口里很有用。比如有些参数不是单字段判断,而是互相制约:
开启重试时, retry必须大于 0关闭任务时,通知邮箱可以为空 mode=delay时,必须传delay_seconds
这种逻辑你继续堆 if 也能写,但维护起来很烦。voluptuous 可以直接加自定义校验函数。
from voluptuous import Invaliddefcheck_job_rule(data):
if data.get("enabled") and data.get("retry", 0) == 0:
raise Invalid("启用状态下 retry 不能为 0")
return data
job_schema = Schema(All({
Required("task_name"): str,
Optional("retry", default=1): int,
Optional("enabled", default=True): bool,
}, check_job_rule))
这个味道就对了。基础字段校验交给 schema,跨字段规则单独收口,不容易乱。
我自己更常用它的地方,其实不是 Web 表单,而是配置文件校验。这玩意线上特别常见,也特别容易埋雷。YAML、JSON、Toml 读出来都能变成 Python dict,但能读出来,不代表能跑。
比如一个导入脚本配置:
config = {
"source": {
"host": "127.0.0.1",
"port": 3306
},
"batch_size": 500,
"dry_run": False
}
很多项目这里默认直接 config["source"]["host"] 往下用了,等出问题再看日志。其实启动时先校验一遍,能省不少脏活。
from voluptuous import Schema, Required, Optional, Rangeconfig_schema = Schema({
Required("source"): {
Required("host"): str,
Required("port"): All(int, Range(min=1, max=65535)),
},
Optional("batch_size", default=200): All(int, Range(min=1, max=5000)),
Optional("dry_run", default=False): bool,
})
cfg = config_schema(config)
print(cfg)
这种写法有两个实际好处。
第一,配置缺字段时会直接报错,不会等到任务跑了十分钟才炸。 第二,默认值能顺手补进去,后面业务代码不用满地写 get("xxx", default)。
再往前走一步,它还适合清洗一些“脏输入”。比如老系统回传的数据,数字字段经常给你字符串,"3"、"15" 这种很常见。你完全可以在 schema 里顺手做转换。
defto_int(v):
return int(v)clean_schema = Schema({
Required("page"): All(to_int, Range(min=1)),
Required("page_size"): All(to_int, Range(min=1, max=100)),
})
params = {
"page": "2",
"page_size": "20"
}
print(clean_schema(params))
这里不是为了炫技,是因为这类转换如果散在 controller、service、dao 各层,到后面谁都说不清楚“这个字段到底在哪一层变成 int 的”。
当然,voluptuous 也不是没脾气。
它更适合做轻量、直接、偏工程实用的校验,不是那种要自动生成 OpenAPI、深度绑定模型、顺带搞序列化反序列化全家桶的路子。你要的是快速把 schema 立起来,拦住脏数据,它很好使。你要的是一整套大型数据建模能力,那它就不是主战场。
还有一点很现实:别把 schema 写成“新一层业务逻辑”。 校验就是校验,主要拦格式、范围、必要字段、简单规则。太重的业务判断还是放业务层,不然 schema 会越来越像一坨难懂的规则引擎。
我比较喜欢它的原因,说到底就一句:该在入口拦住的东西,别放进业务代码里再慢慢腐烂。
很多 Python 项目后面越写越乱,不是业务本身多难,而是输入不干净,边界不清楚,大家默认“先跑起来再说”。voluptuous 干的就是这个脏活:在数据刚进门的时候,先把门槛立住。
这种库平时不显山不露水,真到了接口越来越多、配置越来越杂、脚本越来越散的时候,你会发现它挺顶用。至少比一堆手写 if/else 看着顺眼,也比线上报一个 KeyError 再回头翻数据体面得多。