moonschema

    JSON Schema 2020-12 validator with a zod-style builder API for MoonBit — one engine, two frontends (ajv / zod), WASM-first.

    json-schema
    validation
    schema
    api
    zod
    ajv
    wasm
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    17 hours ago
    Downloads
    1

    #moonschema

    一个引擎、两种 API:用纯 MoonBit 实现的 JSON Schema 2020-12 校验引擎,同时提供 ajv 风格的标准模式编译与 zod 风格的流式构建器。编译到 WASM-GC / JS / Native 三目标,天然面向 Web 与服务端双场景。

    本项目为 2026 MoonBit 国产基础软件开源大赛 参赛项目。

    #为什么是 moonschema

    • 两层 API,一个引擎:ajv 的 "编译 JSON Schema 文档 → 复用校验器" 适合前后端契约校验;zod 的 "字段默认必填 + 流式约束" 适合 MoonBit 业务代码内联建模。两者编译到同一份内部表示,语义完全一致。
    • 跨字段动态规则 DSL"end > start""items[0].price * items[0].qty == total" 这类跨字段约束,表达式在编译期解析(语法错误即刻暴露),弥补声明式 JSON Schema 表达不了的表单级规则。
    • 错误看得见、可中文化:每条错误携带 keywordinstance_path(RFC 6901 JSON Pointer)、schema_path 三元组;校验消息内置中英双语(locale=ZH 一行切换)。
    • strict 模式(ajv 同款默认行为):拼错的关键词在编译期报错,而不是被静默忽略。
    • 递归 $ref 可用:树/链表等递归模式通过惰性解析 + 编译缓存支持。
    • 纯 MoonBit、零第三方依赖:仅依赖标准库 moonbitlang/core,天然跨 WASM-GC / JS / Native。

    #安装

    moon add QuietlyChan/moonschema

    #快速开始

    #ajv 风格:编译标准 JSON Schema

    // moon.pkg: import "QuietlyChan/moonschema" @moonschema
    let schema_text =
    #|{
    #| "type": "object",
    #| "required": ["id", "email"],
    #| "properties": {
    #| "id": { "type": "integer", "minimum": 1 },
    #| "email": { "type": "string" }
    #| },
    #| "additionalProperties": false
    #|}
    let v = @moonschema.compile(@moonschema.parse(schema_text))
    let req =
    #|{ "id": -3, "email": 42, "extra": true }
    let ok = v.check(@moonschema.parse(req)) // false
    println(@moonschema.validate_to_string(v, @moonschema.parse(req)))
    // /id: must be >= 1 [minimum @ #/properties/id/minimum]
    // /email: must be string [type @ #/properties/email/type]
    // (root): must NOT have additional properties ("extra") [additionalProperties @ #/additionalProperties]

    可运行示例:moon run cmd/demo

    #zod 风格:流式构建器

    // moon.pkg: import "QuietlyChan/moonschema/builder" @builder
    let user = @builder.object({
    "name": @builder.string().min_len(1).max_len(50), // 默认必填,与 zod 一致
    "age": @builder.integer().min(0).max(150).optional(),
    "email": @builder.string().email(), // builder 层默认断言 format
    }).strict() // 拒绝未声明字段

    let v = user.compile() // moonschema 校验器
    let doc = user.to_schema() // 标准 JSON Schema 2020-12 文档,可与任何语言互通

    #校验结果的三种消费方式

    let ok : Bool = v.check(instance) // 只要结论
    let errors : Array[@moonschema.ValidationError] = v.validate(instance)
    for e in errors {
    println("\{e.instance_path}: \{e.message}") // 结构化消费
    }

    #在浏览器中使用(Playground)

    Playground 将引擎编译为 JS(ESM)模块在浏览器直接运行:

    bash playground/build.sh # moon build --target js --release --strip + 拷贝产物 cd playground/web && python -m http.server 8080 # 或 npx serve . # 打开 http://localhost:8080

    页面提供三个可编辑预设(基础关键词 / x-rules 跨字段规则 / $ref 递归),实时校验并以表格展示 instance_path / schema_path 双路径错误。已在 Chromium 实测。

    宿主侧集成 API(playground/api.mbt,经 moon.pkglink: { js: { exports: [...] } } 导出):

    导出函数用途
    validate_json(schemaText, dataText)一次调用完成解析→编译→校验,返回结构化 JSON 结果
    compile_schema(schemaText) -> handleajv 式编译一次(句柄,失败 -1)
    check_with(handle, dataText) -> bool热路径布尔判定
    validate_with(handle, dataText) -> string校验并返回完整错误 JSON

    关于 wasm-gc:引擎已验证可在 wasm-gc 目标编译并导出数值函数(link.wasm-gc.exports,Node 24 WebAssembly.instantiate 实测通过);但字符串在 wasm-gc 边界是 GC 对象,对 JS 不透明,需 JS-string-builtins 方案——Playground 因此选择字符串原生互通的 JS 后端。

    #基准测试(vs ajv / zod)

    Node 24,订单式嵌套 schema,每轮 5000 实例 × 7 轮取最优(cd benchmark && npm i && node bench.mjs 复现,详见 benchmark/RESULTS.md):

    实现 / 工作负载ops/sµs per validate
    ajv valid (预解析对象)3,066,1680.33
    zod valid (预解析对象)456,9882.19
    ajv valid (+JSON.parse)400,8982.49
    moonschema valid (字符串入口)89,17711.21
    zod invalid (预解析对象)155,4396.43
    moonschema invalid (字符串+错误报告)61,07816.37

    编译期一次性成本:moonschema compile_schema 6.4ms vs ajv compile 49.9ms——快约 8 倍(树编译 vs 代码生成)。吞吐方面 ajv 的代码生成在 V8 上仍是天花板;moonschema 当前为树解释式执行,字符串入口口径与 zod+parse 同量级,优化空间见路线图。

    #性质测试(roundtrip)

    builder/roundtrip_wbtest.mbt:种子化 LCG 生成 1100 个随机 JSON 实例(覆盖全部类型与嵌套),验证三条性质——to_schema() 文档序列化往返后判定一致、重复编译判定确定、check(fast path)与 validate(错误收集)互恰。

    #已支持的关键词

    类别关键词
    核心type(含数组形式)、enumconst$ref(本地 JSON Pointer,支持递归)、$defs、布尔模式 true/false
    数值minimummaximumexclusiveMinimumexclusiveMaximummultipleOf(浮点容差判定)
    字符串minLengthmaxLength(按 Unicode 码点计数)、pattern(基于 core 正则引擎)、format(默认 annotation;assert_format 开启后断言 email / uuid / ipv4)
    数组itemsprefixItemsminItemsmaxItemsuniqueItemscontainsminContainsmaxContains
    对象propertiespatternPropertiesrequiredadditionalPropertiespropertyNamesminPropertiesmaxPropertiesdependentRequireddependentSchemas
    组合allOfanyOfoneOfnotif/then/else
    扩展x-rules:跨字段动态规则 DSL(见下节)
    编译选项strict(未知关键词报错,x- 前缀扩展放行)、assert_formatlocale(错误消息 EN/ZH)

    暂不支持(编译期明确报错而非静默跳过):远程 $ref(http/https)、命名 fragment 引用($anchor / $dynamicRef)、子模式中的 $id(base URI 变更)、unevaluatedProperties / unevaluatedItems。draft-07 的 items 数组形式会给出迁移到 prefixItems 的提示。

    注:multipleOf 使用浮点商的相对容差判定,0.0075 % 0.0001 这类十进制直觉场景不会因 IEEE 754 精度噪声误判。

    #跨字段规则 DSL(x-rules

    声明式 JSON Schema 表达不了 "结束日期晚于开始日期" 这类表单级约束,moonschema 用一条表达式 DSL 补齐:

    // builder 侧
    let order = @builder.object({
    "start": @builder.string(),
    "end": @builder.string(),
    "total": @builder.number(),
    "items": @builder.array(@builder.object({
    "price": @builder.number(), "qty": @builder.number(),
    }).optional()),
    }).satisfy("end > start")
    .satisfy("items[0].price * items[0].qty == total")

    // JSON Schema 侧等价写法
    // { "x-rules": ["end > start", "items[0].price * items[0].qty == total"] }

    类别运算符
    比较== != < <= > >=(数值按大小、字符串按字典序——ISO 日期可直接比较;跨类型为假)
    算术+ - * / %(除零等产生非有限值时规则不可满足)
    逻辑&& \|\| !(短路)
    字面量数字、字符串("...")、true / false / null
    路径字段名、点号嵌套(address.city)、数组下标(items[0].price

    语义要点:

    • 缺失即藐视通过:表达式引用的路径全部存在才参与判定——可选项的成对约束只约束"存在"的场合,是否必填仍由默认必填 / .optional() / required 表达
    • 编译期语法检查:规则在 compile() 时解析,拼写错误立刻暴露,而不是等到校验期静默失效
    • 错误的 schema_path 精确到 #/x-rules/<序号>

    #错误消息 i18n

    // JSON Schema 侧
    let v = compile(schema, options=CompileOptions::new(locale=ZH))
    // builder 侧
    let v = user.compile(locale=ZH)

    (root): 缺少必需属性 "name" [required @ #/required] /age: 必须 >= 0 [minimum @ #/properties/age/minimum] (root): 规则 "age > 0" 未满足 [x-rules @ #/x-rules/1]

    默认英文;Locale::EN / Locale::ZH 内置,消息本地化只影响校验结果(模式编译错误面向开发者,始终英文)。

    #官方一致性测试

    内置 JSON-Schema-Test-Suite draft2020-12 全量测试(cmd/conformance/suite_data.mbt,由脚本生成),运行 moon run cmd/conformance

    groups: 384 (compile-rejected: 133) pass: 974 fail: 1 skip: 326 pass rate (of judged 975): 99.90%

    • skip (326):模式使用了引擎暂不支持的关键词(unevaluated*$dynamicRef、远程引用等),strict 模式编译期整组拒绝,透明计入而非伪装成失败
    • fail (1):自定义元场景表(vocabulary)语义——v0 不做元模式感知,属已知边界

    #与 ajv / zod 的 API 对应

    moonschemaajvzod
    compile(schemaDoc)ajv.compile(schema)
    v.validate(x) + errors 数组validate(x) + ajv.errorsschema.safeParse(x)
    v.check(x)validate(x) 返回值schema.check(x)
    @builder.object({...})z.object({...})
    .optional() / .nullable().optional() / .nullable()
    .strict() / .catchall(s)additionalProperties: false.strict() / .catchall()
    .enum_(vals) / .literal(v)enum / constz.enum / z.literal
    .email() / .uuid() / .ipv4()format: "email" + ajv-formats.email() / .uuid()

    #关于 WASM 性能

    MoonBit 的首选目标 WASM-GC 让本库可以被 JS 前端以近原生速度调用(无 JIT 预热、内存布局紧凑、校验热路径为纯计算)。roadmap 中把对 ajv / zod 的基准测试作为一等交付物:同一组 schema + 数据集,在 JS 后端与 WASM-GC 后端分别跑分,用数据说话。测试套件已在 native 与 wasm-gc 双目标下全部通过。

    #开发

    moon check # 静态检查 moon test # 运行测试(native) moon test --target wasm-gc # 运行测试(WASM-GC) moon run cmd/demo # 可运行示例 moon fmt && moon info # 格式化 + 更新包接口

    #路线图

    #License

    Schema

    编译产物的类型别名,便于使用者标注签名。

    ValidationError

    校验错误类型别名。

    compile

    ajv 风格入口:把一份 JSON Schema 文档编译为可复用的 Schema

    编译一次、校验任意多次;布尔模式 true / false 也是合法 schema。

    let v = compile(
    parse!(#|{ "type": "object", "required": ["id"] }#|),
    )
    v.check(parse!(#|{"id": 1}#|)) // true

    parse

    fn parse(text : String) -> Json raise
    ParseError

    解析 JSON 文本为 Json 值。

    在 core @json.parse 之上修复整数路径 |n| >= 2^53 误产出 Infinity 的 上游 bug(见 schema/api.mbt),语义与 JS 的 JSON.parse 一致。

    validate_to_string

    fn validate_to_string(v :
    Schema
    , instance : Json) -> String

    校验并把错误渲染为多行文本(演示 / 日志 / 测试快照友好)。

    Source Files