如何在TypeBox+AJV中为嵌套且复用公共Schema的结构添加additionalProperties约束
我明白你遇到的问题了——当用TypeBox的Type.Intersect复用公共查询参数Schema时,直接给Intersect加{ additionalProperties: false }会把所有合并进来的公共参数都误判为“额外属性”,完全没法正常校验。这是因为Type.Intersect的约束逻辑是同时满足所有传入的Schema规则,而不是把多个Object的属性合并成一个新结构,所以给它加additionalProperties: false会导致整个交叉结构里的所有属性都被当成不符合规则的额外项。
下面给你两个可行的解决方案,既能复用公共Schema,又能正确启用additionalProperties: false约束:
方案1:用Type.Composite替代Type.Intersect合并Object Schema
TypeBox提供了Type.Composite专门用来合并多个Object类型的Schema,它会把所有传入的Object的属性合并成一个单一的Object结构,这时候再添加additionalProperties: false就会正确校验合并后的结构不允许额外属性,完美适配你的复用场景。
修改后的查询Schema代码如下:
export const getUserRequestQuerySchema = Type.Composite( [ Type.Object({ status: Type.String() }), commonQueryParamsWithPagination ], { additionalProperties: false } // 这里加约束就会生效了 );
这样配置后,查询参数只允许status、sort、page、size这几个属性,任何额外的查询参数都会被AJV拦截,同时你也保留了对commonQueryParamsWithPagination的复用。
方案2:给公共Schema本身添加封闭约束
如果你因为某些原因一定要用Type.Intersect,可以先给公共参数Schema加上additionalProperties: false,让它本身成为一个封闭的结构,再和自定义的Object Schema做交叉:
首先修改公共参数Schema:
export const commonQueryParamsWithPagination = Type.Object({ sort: Type.Optional(Type.String()), page: numericString({ default: '1' }), size: numericString({ default: '100' }) }, { additionalProperties: false }); // 公共参数自身封闭
然后修改查询Schema:
export const getUserRequestQuerySchema = Type.Intersect( [ Type.Object({ status: Type.String() }, { additionalProperties: false }), // 自定义参数也封闭 commonQueryParamsWithPagination ] );
这种方式下,两个交叉的Object都是封闭的,任何额外属性都会被各自的Schema拦截,间接实现了整个查询参数的封闭约束。不过这种方案的灵活性不如方案1,因为每个交叉的Schema都要单独加约束,不如Type.Composite来得直观。
最后,你的顶层getUserRequestSchema可以保持原来的配置不变,这样顶层和查询层的额外属性都会被正确拦截。
备注:内容来源于stack exchange,提问作者Mr X

