Skip to content

A json_schema validation rule's format keyword is silently IGNORED — ajv runs without ajv-formats, so format: 'email' enforces nothing #5029

Description

@xuyushun441-sys

Found while implementing #4762 (PR #5027) — filed unassigned, deliberately not fixed there: #4762's ruling scoped that PR to the two artifacts that fail to compile, and this is the opposite case (the schema compiles perfectly and one of its keywords is dropped).

What is unenforced

packages/objectql/src/validation/rule-validator.ts builds its shared ajv as:

const ajv = new Ajv({ allErrors: true, strict: false });

Nothing calls addFormats(ajv) and ajv-formats is not a dependency of @objectstack/objectql. In ajv 8 the format keyword is not built in — it ships in the separate ajv-formats package. Under strict: false an unknown format is not an error: ajv logs one line and ignores the keyword.

So a json_schema validation rule like this:

{
  type: 'json_schema',
  name: 'support_config_shape',
  field: 'support_config',
  message: 'Support config must carry a valid contact email.',
  schema: {
    type: 'object',
    properties: { email: { type: 'string', format: 'email' } },
    required: ['email'],
  },
}

…compiles fine, runs on every write, enforces type and required — and enforces nothing at all for format. Reproduced with the repo's own ajv version and the runtime's own options:

$ node -e "
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true, strict: false });
const v = ajv.compile({ type: 'object', properties: { email: { type: 'string', format: 'email' } }, required: ['email'] });
console.log('compiles OK:', typeof v === 'function');
console.log('validates {email: not-an-email} =', v({ email: 'not-an-email' }));
"
unknown format "email" ignored in schema at path "#/properties/email"
compiles OK: true
validates {email: not-an-email} = true

Why this is the #4649 / #4762 family, one level in

Same shape, smaller blast radius: the rule is declared, it appears in the metadata, it appears in any "what protects this object" listing, it even runs — and one of the constraints the author wrote is silently dropped for every record. The only signal is a stderr line from ajv that names no rule and no object, emitted once at compile time. format is one of the most reached-for JSON Schema keywords (email, uri, uuid, date, date-time, ipv4), so this is not an exotic corner.

Note the failure is partial, which is what makes it nastier than #4762's: the rule visibly rejects bad type/required payloads in dev, so it reads as working, while the format half never fires.

Why #4762's publish gate does not catch it

#4762 (PR #5027) added validateRuleCompilability, which compiles every json_schema rule's schema with the same ajv options the runtime uses and rejects at authoring time what does not compile. That parity is deliberate and load-bearing — but it means a schema whose format the runtime ignores is a schema the gate also compiles happily. Correct behaviour for that gate; this is a different question, and it needs its own decision.

The decision this needs (not obvious — please rule)

  1. Register ajv-formats in the runtime (addFormats(ajv)), making format actually enforced. Closes declared ≠ enforced in the direction authors expect. Cost: a new runtime dependency, and it is a behaviour change on deployed data — records that pass today can start failing, so it needs a changeset saying so loudly. Also raises "which format set?" (ajv-formats full vs fast mode).
  2. Reject format at authoring time — the publish gate refuses a json_schema schema containing a format keyword, with a message saying the runtime does not enforce it and pointing at the format validation-rule type (type: 'format' with regex / a named format), which is enforced. Keeps the runtime untouched and keeps "declared = enforced" true by removing the declaration. Cost: rejects schemas that are valid JSON Schema, which authors will find surprising, and it is a narrowing of the authorable surface.
  3. Do nothing but document it. Named for completeness; it is the option that keeps a silent gap, so it should probably lose.

Recommendation: (1) if the maintainer is willing to accept a behaviour change on existing data, because it makes the metadata mean what it says; otherwise (2), which is the ADR-0049 enforce-or-remove answer applied to a keyword rather than a property. What should not happen is the current state, where the keyword is authorable, documented by JSON Schema itself, and inert.

Acceptance sketch

  • A json_schema rule whose schema carries format: 'email' either rejects not-an-email at write time (option 1), or is refused at publish with a message naming the rule, the object and the keyword (option 2).
  • Whichever is chosen, a test pins it, and the #4649 / #4762 family gets one more closed case rather than a fourth open one.

Activity

  1. self-assigned this
    on Aug 4, 2026
  2. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    Contributor

    认领:PM 循环第 1 轮
    会话:session_01Pbu27iNUfQCHeuS551Rqo7
    分支:claude/issue-5029-ajv-formats-enforced
    Worktree:objectstack-issue-5029
    域:domain:engine
    文件面:packages/objectql/src/validation/rule-validator.ts、packages/objectql/package.json + pnpm-lock.yaml(新增 ajv-formats 依赖)、packages/lint/src/validate-rule-compilability.ts(仅 ajv 环境对齐的最小 parity 触碰,跨入 devx 面,已声明)、测试 + changeset(越界即停,报告说明)

    PM 裁定(否决窗口,维护者可叫停):取正文方案 1(运行时注册 ajv-formats,format 真实强制)。两轴理由:长远上 format 是 JSON Schema 标准键,强制它让元数据「说什么就是什么」,而方案 2 拒收合法 JSON Schema、把作者推向私有写法;防 AI 写错上,AI 写 format: 'email' 是最高频形状,方案 1 让声明即兑现。行为变更(存量能过的记录今后写入可能被拒)必须在 changeset 里响亮写明;format 集取 ajv-formats 默认(full)。#4762 的 publish 门禁(packages/lint)与运行时 ajv 环境同步加 formats 保持 parity。若实现中发现该变更破坏仓内 examples/存量测试到需要语义抉择的程度,停下回报 needs_decision。


    Generated by Claude Code

  3. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    Contributor

    验收:ACCEPT —— PR #5182(等 CI 绿后入队)

    会话:session_01Pbu27iNUfQCHeuS551Rqo7(PM 循环第 1 轮)

    落地内容:运行时 rule-validator.ts 注册 ajv-formats(默认 full 集),format 关键字真实强制;#4762 发布门禁同环境注册同插件 —— 且 dev 证明了这一步是承重的而非装饰:ajv-formats 还带 formatMinimum/formatMaximum 关键字,门禁不装插件会放行一条运行时拒编译的 schema(两个方向都有测试钉住)。ajv-formats 进两包的 LAZY_DEPS 启动契约,内核路径零开销。changeset 按裁定响亮标注存量数据行为变更。

    实况核验:draft ✓ / Fixes 行 ✓ / 10 文件全在声明文件面内 ✓ / 无 packages/spec、无 releases 触碰 ✓ / 仓内扫描无 format-bearing 存量规则,零语义抉择 ✓ / objectql 1826 + lint 1184 + showcase + dogfood 全绿(报告含完整命令与输出)。

    残留边界:拼错的 format 名仍被静默忽略 —— 已由 dev 单独立卡 #5178(附 Blocked-by 本单),两包测试钉住现状,翻转必须是自觉动作。已分诊转 devx 车道队列。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions