🌐 语言 / Language:简体中文 | English | 繁體中文
零依赖、Pythonic、可链式调用的运行时 Schema 校验工具库:把不可信的 JSON、表单、环境变量与消息载荷,一次性校验、归一化成你想要的数据。
Valkit 是一个纯 Python 标准库实现的运行时数据校验工具库(runtime validation toolkit)。你用一套流式(fluent)API 声明数据形状,Valkit 负责:
- ✅ 在运行时拦截非法输入(请求体、配置文件、消息队列消息、外部 API 响应……)
- 🔧 归一化(normalize):去空白、大小写转换、字符串转数字/布尔、默认值填充
- 📍 一次性返回带精确路径的结构化错误清单,而不是只报第一个错
- 📤 一键导出 JSON Schema 2020-12,方便与前端 / 其他语言团队共享契约
- 🔌 附带环境变量解析器与命令行工具,覆盖后端开发最常见的三个入口
| 痛点 | 手写 if/else |
重型建模库 | Valkit |
|---|---|---|---|
| 嵌套结构报错定位 | 自己拼路径 | 错误结构偏重 | 内置 users[0].name 式路径 |
| 部署体积 | 无依赖但难维护 | 依赖链长、冷启动慢 | 零运行时依赖,纯标准库 |
| 表单/环境变量字符串转类型 | 到处 int(...) try/except |
需额外胶水代码 | coerce=True 一键宽松转换 |
| 一次收集全部错误 | 自己实现 | 支持 | 默认收集全部问题 |
| 复用与派生 Schema | 重复代码 | 继承体系复杂 | 链式不可变,pick/omit/partial 秒派生 |
- 零运行时依赖:整个库只依赖 Python 标准库,
pip install后没有任何传递依赖,适合 Serverless / 边缘 / 内网环境。 - 不可变链式 Schema:每个链式方法都返回新对象,基础 Schema 可安全复用、派生,不会被下游调用悄悄改坏。
- 双模式错误处理:
parse()直接抛异常;safe_parse()返回带ok/value/error的结果对象,业务代码各取所需。 - 宽松输入强类型输出:
coerce模式支持"42"→42、"true"→True、2.0→2等真实世界脏数据转换。 - 内置环境变量加载器:
v.env(prefix="APP_")声明式读取配置,缺失、越界、格式错误一次性聚合抛出。 - JSON Schema 双向打通:
to_json_schema()把 Python Schema 导出为标准 draft 2020-12 文档。 - 自带 CLI:
valkit validate schema.py:User payload.json可直接在 CI 流水线里校验 JSON 文件。
💡 灵感来源:开发者体验参考了 TypeScript 生态的 zod「声明即校验、类型可推导」的产品思路;但 Valkit 没有复制其任何源码,所有架构、API 细节与实现均为面向 Python 语言习惯独立自研,并额外补齐了环境变量解析、CLI、JSON Schema 导出等差异化能力。
- 🧩 完整的基础类型:
str / int / float / number / bool / date / datetime / literal / enum / none / any / unknown / never - 📦 丰富的容器类型:
object / array / tuple / record / union / discriminated_union / intersection - 📏 细粒度约束:长度、区间、正则、前后缀、
multiple_of、唯一性、枚举、精确字面量 - 📮 内置格式校验(零依赖):
email / url / uuid / ipv4 / ipv6 / iso_date / iso_datetime / semver / slug - 🧼 归一化管线:
trim / lowercase / uppercase / transform / pipe - 🎯 自定义规则:
refine(fn, message, code),抛ValueError也会被转成结构化问题 - 🧱 对象派生:
pick / omit / partial / required / extend / merge,以及strip / strict / passthrough三种未知键策略 - 🧪 判别联合(Discriminated Union):消息 / 事件场景按字面量标签快速分发,报错更精准
- 🧾 结构化错误:每个问题都带
path / code / message,可直接序列化为 JSON - 🔄 宽松强制转换:
parse(data, coerce=True)处理表单、query string、环境变量等字符串输入 - 📄 JSON Schema 导出:draft 2020-12,含
required / additionalProperties / discriminator等 - 🌍 环境变量解析:带前缀、默认值、必填聚合、类型强转
- 💻 命令行工具:CI 友好,退出码
0=通过 / 1=校验失败 / 2=用法错误 - ♻️ 完全跨平台:纯 Python,Windows / macOS / Linux 行为一致;支持 Python 3.8 ~ 3.13
- 🐍 Python 3.8 或更高版本
- 📦 运行时依赖:无(开发/测试需要
pytest,属于可选依赖)
从源码构建 wheel 后安装(推荐,离线可用):
# 克隆后本地构建
git clone https://github.com/gitstq/valkit.git
cd valkit
python3 -m pip install build
python3 -m build
python3 -m pip install dist/valkit-1.0.0-py3-none-any.whl或以可编辑模式参与开发:
python3 -m pip install -e ".[dev]"from valkit import v, ValidationError
Signup = v.object({
"username": v.str().min_len(3).max_len(20).slug(),
"email": v.str().email(),
"age": v.int().gte(18).lte(120).optional(),
"role": v.enum(["admin", "member"]).default("member"),
})
# parse():失败抛 ValidationError,成功返回归一化后的数据
user = Signup.parse({
"username": "Ada-Lovelace",
"email": "ada@example.com",
"age": "36", # 表单里的字符串
}, coerce=True)
print(user)
# {'username': 'Ada-Lovelace', 'email': 'ada@example.com', 'age': 36, 'role': 'member'}
# safe_parse():不抛异常,自行分支
result = Signup.safe_parse({"username": "x", "email": "bad"})
if not result:
for issue in result.error.issues:
print(issue.path_str, issue.code, issue.message)
# username min_length string must be at least 3 characters, got 1
# email format invalid email format运行仓库内示例:
python examples/quickstart.py
python examples/env_demo.pyfrom valkit import v
name = v.str().trim().min_len(1).max_len(50)
score = v.int().gte(0).lte(100)
ratio = v.number().finite() # 拒绝 NaN / inf
flag = v.bool()
day = v.date() # 接受 date 或 "YYYY-MM-DD"
moment = v.datetime() # 接受 datetime 或 ISO 8601 字符串
level = v.enum(["low", "high"]) # 也支持 Python Enum 类
kind = v.literal("csv") # 精确字面量(区分 1 与 True)
ip = v.str().ipv4()
ver = v.str().semver()字符串约束:min_len / max_len / length / nonempty / regex / starts_with / ends_with / includes / trim / lowercase / uppercase,以及格式方法 email / url / uuid / ipv4 / ipv6 / semver / slug / format(name)。
数字约束:gt / gte / lt / lte / positive / nonnegative / negative / multiple_of / finite。
Base = v.object({"id": v.int(), "name": v.str(), "note": v.str().optional()})
Base.parse({"id": 1, "name": "x", "extra": 9})
# 默认 strip:{'id': 1, 'name': 'x'} —— extra 被丢弃
Base.strict() # 未知键直接报错(additionalProperties=False)
Base.passthrough() # 保留未知键
Base.pick(["id"]) # -> 只含 id
Base.omit(["note"]) # -> 去掉 note
Base.partial() # -> 所有字段变可选
Base.partial(["name"]) # -> 仅 name 变可选
Base.extend({"age": v.int()}) # -> 追加字段
Base.merge(other_object) # -> 合并两个对象 Schemav.array(v.int()).min_len(1).max_len(10).nonempty().unique()
v.tuple([v.str(), v.int(), v.bool()]) # 定长、按位置校验
v.record(v.str().slug(), v.number().gte(0)) # 键、值分别约束# 普通联合:依次尝试,命中任一即可
id_schema = v.union([v.int(), v.str().uuid()])
id_schema = v.int() | v.str() # 等价写法
# 判别联合:按 type 字段分发,事件/消息建模首选
Event = v.discriminated_union("type", [
v.object({"type": v.literal("created"), "name": v.str()}),
v.object({"type": v.literal("deleted"), "id": v.int()}),
])| 修饰 | 允许键缺失 | 允许值为 None |
说明 |
|---|---|---|---|
.optional() |
✅ | ❌ | 对应 TypeScript 的 ?: |
.nullable() |
❌ | ✅ | 值可以显式为 None |
.nullish() |
✅ | ✅ | 两者都允许 |
.default(x) |
✅(自动) | ❌ | 缺失时填入 x;x 为函数时惰性调用 |
Settings = v.object({
"host": v.str().default("127.0.0.1"),
"port": v.int().default(lambda: 8080), # 工厂函数,每次 parse 独立生成
"note": v.str().nullish(),
})
Settings.parse({}) # {'host': '127.0.0.1', 'port': 8080}even = v.int().refine(lambda x: x % 2 == 0, message="must be even", code="even")
cleaned = (
v.str()
.transform(str.strip)
.transform(str.lower)
.pipe(v.str().min_len(2)) # 归一化后再交给下一个 Schema
)
cleaned.parse(" HELLO ") # 'hello'result = schema.safe_parse(bad_data)
for issue in result.error.issues:
issue.path # ('users', 0, 'name') 元组路径
issue.path_str # 'users[0].name'
issue.code # 'min_length' / 'format' / 'required' ...
issue.message # 人类可读信息
result.error.to_dicts() # 直接 json.dumps 输出给前端 / 日志from valkit import to_json_schema
import json
print(json.dumps(to_json_schema(Signup, title="Signup"), indent=2, ensure_ascii=False))env = v.env(prefix="APP_") # 默认读取 os.environ
env.str("HOST", default="0.0.0.0")
env.int("PORT", default=8080, ge=1, le=65535)
env.bool("DEBUG", default=False)
env.enum("LOG_LEVEL", ["debug", "info", "error"], default="info")
env.url("PUBLIC_API", required=True)
config = env.load() # 缺失必填 / 类型错误会一次性聚合抛出# 校验 JSON 文件(--coerce 开启宽松转换)
valkit validate examples/cli_schemas.py:Signup examples/signup_ok.json --coerce
# 导出 JSON Schema
valkit jsonschema examples/cli_schemas.py:Signup -o signup.schema.json
# 不安装也可用模块方式运行
python -m valkit validate ./schemas.py:User ./payload.json退出码:0 校验通过;1 数据校验失败(问题以 JSON 打到 stderr);2 文件/参数/Schema 加载错误。
- Web 框架入参校验:在 Flask / FastAPI / Django 的视图边界调用
schema.parse(request.get_json()),非法请求统一返回结构化错误。 - 配置与密钥管理:用
v.env()取代散落各处的os.getenv+int(),启动即校验,杜绝线上配置事故。 - 消息队列 / Webhook 事件:用
discriminated_union建模不同type的事件载荷。 - 外部 API 响应防御:第三方数据结构不可信,先过 Schema 再进业务逻辑。
- CI 数据契约检查:用 CLI 在流水线中校验示例 JSON、生成 JSON Schema 产物。
🖼️ 演示截图 / GIF 占位:终端演示录屏请放置于
docs/demo/目录(如docs/demo/quickstart.svg),文档此处预留引用位置:。
- 库不是框架:不绑定任何 Web / ORM / 序列化框架,任何 Python 程序都能在边界处随手使用。
- 默认收集全部错误:真实用户宁可一次改完所有问题,也不想反复提交、每次只看到一个错。
- 归一化与校验同管线:
trim、大小写、强转、默认值、自定义transform按声明顺序执行,输出即可直接入库。 - Schema 不可变:链式方法返回副本,基础 Schema 可以在模块顶层定义、到处复用、安全派生。
- 零依赖是特性不是约束:格式校验(邮箱/URL/IP/UUID/日期/SemVer)全部基于标准库实现,可在内网与离线环境自由分发。
- 纯标准库 + src 布局:零传递依赖、类型清晰、打包简单(
py3-none-any通用 wheel)。 - PEP 621
pyproject.toml:现代 Python 打包标准,setuptools 后端保证最大兼容性。 - pytest 参数化测试:70 条用例覆盖类型、容器、管线、环境变量、JSON Schema 导出与 CLI。
- PEP 561
py.typed:内置类型标记,方便接入 mypy / pyright 的工程。
- v1.1:错误信息 i18n(内置中英文模板,可自定义文案表)
- v1.1:更多格式(
base64 / json-web-token / country-code / phone)与异步refine - v1.2:从 JSON Schema / OpenAPI Schema 反向生成 Valkit Schema
- v1.2:性能基准套件与循环引用安全的递归 Schema
- v1.3:发布到 PyPI,提供
pip install valkit一键安装 - v1.3:mypy / pyright 类型推导插件(
parse返回精确类型)
欢迎认领路线图条目,也欢迎补充格式校验器、边界测试用例、更多语言的文档翻译(日语、韩语、西班牙语等)。
Valkit 属于工具库 / 组件库,以源码与 wheel 形式分发,无需可执行 Release 产物。
# Linux / macOS
bash scripts/build.sh
# Windows (cmd / PowerShell)
scripts\build.bat脚本会依次执行:① 全量 pytest → ② 清理旧产物 → ③ 生成 dist/valkit-x.y.z.tar.gz(sdist)与 dist/valkit-x.y.z-py3-none-any.whl(wheel)。
| 项 | 范围 |
|---|---|
| 操作系统 | Windows / macOS / Linux(纯 Python,无原生扩展) |
| Python | 3.8 / 3.9 / 3.10 / 3.11 / 3.12 / 3.13 |
| 运行时依赖 | 无 |
| 开发依赖 | pytest>=7、build(仅构建时) |
| 产物格式 | sdist .tar.gz + universal wheel .whl |
python3 -m pip install /path/to/valkit-1.0.0-py3-none-any.whl或直接把 src/valkit 目录复制进项目(零依赖,单包自洽)。CI 中可使用 CLI 对样例数据做契约校验,详见上文「命令行工具」。
我们欢迎 Issue、PR 与文档翻译!提交前请先阅读完整的 CONTRIBUTING.md。简要规范:
- 🔀 Fork 后新建
feat/xxx、fix/xxx分支; - 🧪 新功能必须附带测试,保证
pytest全绿; - 🧱 不得引入运行时第三方依赖(开发依赖除外);
- 📝 遵循 Angular 提交规范:
feat: 新增功能fix: 修复问题docs: 文档更新refactor: 代码重构test: 测试相关chore: 构建/工程配置
- 💬 Issue 请附最小复现、Python 版本与期望/实际行为。
Q:和 Pydantic 有什么区别? A:Pydantic 是优秀的数据建模框架,功能全面但依赖链与包体更大;Valkit 定位是零依赖、边界处随手用的轻量校验器,不做 ORM、不绑定模型类,适合 Serverless、插件、SDK 与内网受限环境。
Q:optional() 为什么不接受显式 None?
A:这是刻意区分「键缺失」与「值为空」两种语义;想两者都接受请用 .nullish()。
Q:coerce 会不会悄悄吞掉脏数据?
A:不会。强转只覆盖明确安全的场景(数字字符串、常见布尔词、整数值浮点),转换失败仍然报类型错误。
Q:可以校验 dataclass / TypedDict 吗?
A:Valkit 只校验数据本身,不关心容器类型;把对象转成 dict 后交给 Object Schema 即可。
本项目基于 MIT License 开源,允许自由使用、修改、分发与商用,保留版权声明即可。