@ddd-ts-kit/validation

中文文档
0.106.0发布于 2026/11/01MITTypeScriptES Module构建来源已验证

一个用于输入验证的模式契约,带有用于zod、类验证器、TypeBox和标准模式的适配器。

中文使用文档
文档目录7 节
  1. 合同
  2. 在路线上
  3. 萨德
  4. class-validator
  5. TypeBox
  6. 标准模式(validbot, arktype,…)
  7. 共享模式

英语 · 法语

@ddd-ts-kit /验证

一个 Schema 在边界处进行输入验证的契约,带有用于zod的适配器, 类验证器、TypeBox和任何标准模式库。路由,dto和OpenAPI生成器依赖 在契约上,这样验证库可以在不触及它们的情况下进行更改。

业务规则不属于这里:它们存在于域中,从不导入验证库。

A request at the boundary: its params, query, headers and body each go through the Schema the route declares; when every part parses, the handler receives typed values and the domain checks the business rules; when a part fails, its issues are collected with the name of the part in front of their path, such as body.items.0.sku, and the request is refused with one ValidationError, code VALIDATION_FAILED, answered 422 with the list of issues; the same schemas give the JSON Schema of the OpenAPI document

模式检查 形状 输入的内容:类型、必填字段、格式、长度。什么 业务决定(过去的插槽,库存以上的数量)由域检查, 无论进入点是什么:

import { z } from 'zod';
import { DomainRefusal } from '@ddd-ts-kit/kernel';
import { zodSchema } from '@ddd-ts-kit/validation/zod';

// Instead of: a business rule hidden in the schema of one route, which a job or a consumer never meets.
export const bookWithARule = zodSchema(
  z.object({ startsAt: z.iso.datetime() }).refine((body) => Date.parse(body.startsAt) > Date.now(), 'The slot is in the past.'),
);

// Write: the shape in the schema...
export const book = zodSchema(z.object({ startsAt: z.iso.datetime() }));

// ...and the rule in the domain, with its own refusal code.
export const SchedulingCodes = { SlotInThePast: 'SLOT_IN_THE_PAST' } as const;

export class SlotInThePast extends DomainRefusal {
  readonly code = SchedulingCodes.SlotInThePast;

  static atStart(startsAt: Date) {
    return SlotInThePast.refusal(`The slot at ${startsAt.toISOString()} is in the past.`, {
      startsAt: startsAt.toISOString(),
    });
  }
}

export function startingAt(startsAt: Date, now: Date): Date {
  if (startsAt <= now) {
    throw SlotInThePast.atStart(startsAt);
  }

  return startsAt;
}

合同

import { parseOrThrow, type Infer, type Schema } from '@ddd-ts-kit/validation';

interface Schema<T> {
  parse(input: unknown): Result<T, ValidationIssue[]>;
  toJsonSchema(): JsonSchema;
}

type ValidationIssue = { path: string; code: string; message: string }; // path: "items.0.sku"

模式是一个值:每个输入一个值,传递给使用它进行验证的路由或事件。 它从未绑定到容器中,所以它是一个接口,而不是一个可注入端口(抽象) 类):在组合根中没有什么需要声明的。

  • parse 永远不会为无效输入抛出异常:它会返回 err(issues).
  • parseOrThrow(schema, input) 返回值或抛出 ValidationError,一个 DomainError 使用代码 VALIDATION_FAILED 和 issues.
  • Infer<typeof schema> 给出解析后的类型。
  • customSchema(parse, jsonSchema?) 从普通函数构建模式。
  • sharedSchema(name, schema) 记录一个模式一次,在 name (见 共享模式)。
import { err, ok } from '@ddd-ts-kit/kernel';
import { ValidationError, customSchema, parseOrThrow, type Infer } from '@ddd-ts-kit/validation';

// A schema from a plain function: a check written by hand, or a double in a test.
const quantity = customSchema(
  (input: unknown) =>
    typeof input === 'number' && Number.isInteger(input) && input > 0
      ? ok(input)
      : err([{ path: '', code: 'not_a_quantity', message: 'A positive whole number is expected.' }]),
  { type: 'integer', minimum: 1 },
);

type Quantity = Infer<typeof quantity>; // number

export const refused = quantity.parse('3'); // { ok: false, error: [{ path: '', code: 'not_a_quantity', ... }] }
export const three: Quantity = parseOrThrow(quantity, 3); // 3

try {
  parseOrThrow(quantity, 0);
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.code, error.issues); // VALIDATION_FAILED [{ path: '', code: 'not_a_quantity', ... }]
  }
}

根条目不导入第三方包。每个适配器都存在于它自己的子路径和它的 库是可选的对等依赖项:只安装您使用的库。

在路线上

的路线 @ddd-ts-kit/http 为它所读取的请求的每个部分命名一个模式。每一个部分 在处理程序运行之前解析,所有部分的问题一起得到回答,并且每个 Issue path从它的部分开始:

import { z } from 'zod';
import { defineRoute, executeRoute, type RawRequest } from '@ddd-ts-kit/http';
import { zodSchema } from '@ddd-ts-kit/validation/zod';

const placeOrder = defineRoute(
  {
    method: 'POST',
    path: '/orders',
    operationId: 'placeOrder',
    summary: 'Place an order',
    request: {
      body: zodSchema(z.object({ items: z.array(z.object({ sku: z.string().min(1), quantity: z.number().int().positive() })).min(1) })),
    },
    responses: { 201: { description: 'The order is placed.', body: zodSchema(z.object({ lines: z.number() }), { io: 'output' }) } },
    security: false,
  },
  async ({ body }) => ({ status: 201, body: { lines: body.items.length } }), // body is typed: { items: { sku, quantity }[] }
);

const request = (body: unknown): RawRequest => ({ params: {}, query: {}, headers: {}, body, raw: undefined });

export const placed = await executeRoute(placeOrder, request({ items: [{ sku: 'A-1', quantity: 2 }] })); // 201, { lines: 1 }
export const refused = await executeRoute(placeOrder, request({ items: [{ sku: 'A-1', quantity: 0 }] }));
// 422, a problem with the code VALIDATION_FAILED and
// issues: [{ path: 'body.items.0.quantity', code: 'too_small', message: 'Too small: expected number to be >0' }]

处理程序永远不会看到无效请求,路由的OpenAPI文档描述了 同一个身体,来自 toJsonSchema().

萨德

npm i zod
import { z } from 'zod';
import { zodSchema } from '@ddd-ts-kit/validation/zod';

declare const input: unknown; // what the request carried

const placeOrder = zodSchema(
  z.object({
    customerId: z.string().uuid(),
    items: z.array(z.object({ sku: z.string().min(1), quantity: z.number().int().positive() })).min(1),
  }),
);

const result = placeOrder.parse(input);
if (!result.ok) {
  console.log(result.error); // [{ path: 'items.0.sku', code: 'too_small', message: '...' }]
}

JSON模式来自于zod4 z.toJSONSchema (草案2020-12,可在OpenAPI中使用 3.1)。默认情况下,它描述输入端;通过 { io: 'output' } 用于响应体。一个 类型JSON模式不能表示(a) Date,一个 bigint)写为 {} 而不是 辜负了这一代人。

class-validator

npm i class-validator class-transformer class-validator-jsonschema reflect-metadata
import { Type } from 'class-transformer';
import { ArrayMinSize, IsInt, IsString, IsUUID, Min, ValidateNested } from 'class-validator';
import { classSchema } from '@ddd-ts-kit/validation/class-validator';

class OrderItemDto {
  @IsString() declare sku: string;
  @IsInt() @Min(1) declare quantity: number;
}

class PlaceOrderDto {
  @IsUUID() declare customerId: string;

  @ValidateNested({ each: true })
  @Type(() => OrderItemDto)
  @ArrayMinSize(1)
  declare items: OrderItemDto[];
}

export const placeOrder = classSchema(PlaceOrderDto);

parse 返回类的实例。 whitelist 和 forbidNonWhitelisted 都在附近 默认值:未声明的属性是一个问题。覆盖与 classSchema(Dto, { validator: { forbidNonWhitelisted: false }, transform: { ... } }). 嵌套错误被扁平化(items.0.quantity),每个失败的约束有一个问题,即 约束名称为 code. JSON模式是2020-12年的草案,包含嵌套类 可从下面的类访问 $defs.

需要 experimentalDecorators 在项目的 tsconfig.json.

每个字段都是 declareD,没有初值。类转换器保留a的初始值 输入缺少的字段: customerId = '' 下 @IsString() 在没有顾客的情况下会接受订单吗 Pass,好像它的名字是空的一样。可选字段是 @IsOptional() 与 declare note?: string;. classSchema 拒绝其新实例持有带有 DeclarationError 命名它的字段和修复。

声明一次的DTO类在整个(classSchema(SlotDto, { shared: true }),或嵌套 与 @ValidateNested() 和 @Type, @IsDefined() 当需要时:嵌套验证 允许丢失的对象通过),扩展(class BookBody extends SlotDto { ... })或in 部分是通过复制约束、嵌套类型和转换的三个帮助程序完成的 它们所保留的属性:

import { IsEmail, IsIn, IsISO8601, IsString, IsUUID } from 'class-validator';
import { omitClass, partialClass, pickClass } from '@ddd-ts-kit/validation/class-validator';

class AppointmentDto {
  @IsUUID() declare patientId: string;
  @IsISO8601() declare startsAt: string;
  @IsISO8601() declare endsAt: string;
  @IsIn(['booked', 'cancelled']) declare status: 'booked' | 'cancelled';
}

class PatientDto {
  @IsString() declare name: string;
  @IsEmail() declare email: string;
}

export class RescheduleBody extends pickClass(AppointmentDto, ['startsAt', 'endsAt']) {}
export class BookBody extends omitClass(AppointmentDto, ['status']) {}
export class EditPatientBody extends partialClass(PatientDto) {} // every property optional, checked when present

扩展帮助器的类在契约中命名形状。选择一个携带的属性 没有约束被拒绝:验证将把它作为未知剥离。中嵌套的派生类 另一个通过 @Type 声明至少一个自己的属性:class-validator-jsonschema 只从类自己的装饰器记录类。

TypeBox

npm i typebox
import Type from 'typebox';
import { typeboxSchema } from '@ddd-ts-kit/validation/typebox';

export const slot = Type.Object({ startsAt: Type.String({ format: 'date-time' }), minutes: Type.Integer({ default: 30 }) }, { $id: 'Slot' });
export const book = typeboxSchema(Type.Object({ patientId: Type.String({ minLength: 1 }), slot }));
export const page = typeboxSchema(Type.Object({ limit: Type.Integer() }), { coerce: true }); // query strings

架构编译一次(typebox/compile)。 parse 填充默认值并删除 模式没有声明的属性(如zod),然后检查;问题代码是JSON 失败的架构关键字(required, format, minLength……)。 { coerce: true } 转换 首先将文本转换为声明的类型,用于路径参数和查询字符串。TypeBox模式是 JSON模式已经存在: toJsonSchema() 对象构建的嵌套模式 $id 移动 下 $defs 并且被引用,所以它会在合同中出现;具有自己的模式 $id 是 共享。在其路径上报告丢失的属性(slot.startsAt),正如zod所报道的那样 两个不同的模式在一个模式下 $id 是拒绝了。 Type.Pick, Type.Omit 和 Type.Partial 重用模式的一部分,作为一个新的形状; Type.Intersect([slot, Type.Object({...})]) 扩展了它 并将共享部分作为参考(allOf: [{ $ref: Slot }, ...]),而 Type.Interface([slot], {...}) 写入一个新的平面形状。(Type.Composite 属于 @sinclair/typebox 0.34, TypeBox 1中不存在。) 为…而写 typebox 的后续版本 @sinclair/typebox)。

标准模式(validbot, arktype,…)

import * as v from 'valibot';
import { standardSchema } from '@ddd-ts-kit/validation/standard-schema';

export const placeOrder = standardSchema(
  v.object({ customerId: v.pipe(v.string(), v.uuid()) }),
  // Optional: the JSON Schema of the contract, which this adapter does not derive.
  { type: 'object', required: ['customerId'], properties: { customerId: { type: 'string', format: 'uuid' } } },
);

适配器验证 ~standard.validate 标准模式v1。的JSON模式 合同是第二个参数,当它是手写的,或由出口商 你的图书馆);否则就是库在实现同伴时自己转换的对象 标准JSON模式v1 (~standard.jsonSchema;通过zod 4验证,而arktype 并且有效通过 toStandardJsonSchema 的 @valibot/to-json-schema 宣布它,但不是 输入端默认为和 standardSchema(schema, undefined, { io: 'output' }) 得到回应;否则 {}, OpenAPI文档将其显示为“任意值”。图式 库不能以JSON模式编写(zod拒绝) z.date(), z.coerce.date(), z.custom)是 记录 {} 也:传递它的JSON模式作为第二个参数。该规范没有错误代码,所以每个问题 有代码吗? invalid. 异步验证的模式 parse 在边界处不支持Throw: async模式。

共享模式

多个路由使用的模式声明一次,记录一次:的OpenAPI生成器 @ddd-ts-kit/http 写在里面 components.schemas 然后用 $ref 从每一个 路线,整个或嵌套。当模式的JSON模式是对它自己的JSON模式的引用时,模式是共享的 定义({ $ref: '#/$defs/Slot', $defs: { Slot: ... } }),每个库达到它的形式 自己的方式:

A schema shared under the name Slot: its JSON Schema is a reference to its own definition; the routes reschedule and book use it, one whole and one nested in its body; the OpenAPI generator writes Slot once under components.schemas and points at it with $ref from both routes, instead of two copies

import Type from 'typebox';
import * as v from 'valibot';
import { z } from 'zod';
import { sharedSchema } from '@ddd-ts-kit/validation';
import { classSchema } from '@ddd-ts-kit/validation/class-validator';
import { standardSchema } from '@ddd-ts-kit/validation/standard-schema';
import { typeboxSchema } from '@ddd-ts-kit/validation/typebox';
import { zodSchema } from '@ddd-ts-kit/validation/zod';

declare class PatientDto {
  name: string;
  email: string;
}

// zod: an id in its metadata, kept when the schema is nested in another one
const slot = z.object({ startsAt: z.iso.datetime(), endsAt: z.iso.datetime() }).meta({ id: 'Slot' });
export const reschedule = zodSchema(slot);
export const book = zodSchema(z.object({ patientId: z.uuid(), slot }));

// class-validator: the name of the class
export const patient = classSchema(PatientDto, { shared: true });

// TypeBox: an $id in its options, kept when the schema is nested in another one
const money = Type.Object({ cents: Type.Integer(), currency: Type.String() }, { $id: 'Money' });
export const price = typeboxSchema(Type.Object({ amount: money }));

// any schema, the Standard Schema ones included
const amountSchema = v.object({ cents: v.pipe(v.number(), v.integer()) });
export const amount = sharedSchema('Amount', standardSchema(amountSchema));

sharedNameOf(jsonSchema) 回答JSON模式共享的名称,如果有的话:

import { customSchema, sharedNameOf, sharedSchema } from '@ddd-ts-kit/validation';

const amount = sharedSchema('Amount', customSchema((input) => ({ ok: true, value: input }), { type: 'integer' }));

export const jsonSchema = amount.toJsonSchema(); // { $ref: '#/$defs/Amount', $defs: { Amount: { type: 'integer' } } }
export const name = sharedNameOf(jsonSchema); // 'Amount'

schemaBehind.of(schema) 回答模式 sharedSchema 是给定的,那么另一个又是什么呢 附加到它的包(字段) withFieldAccess 的 @ddd-ts-kit/http 储备)用于 也是共享的。

表示层的共享模式位于 generate schema 写在下面 presentation/http/schemas/ 上下文的,上下文下的 presentation/ 共享内核的 有几种情况可以回答这个问题;从来没有在这个领域。

版本与发布记录

@ddd-ts-kit/validation 共发布 63 个版本,首次发布于 2026/10/11;过去 12 个月发布了 63 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.106.0latest2026/11/012 天前
0.105.02026/10/312 天前
0.104.02026/10/312 天前
0.103.02026/10/313 天前
0.102.02026/10/313 天前
0.101.02026/10/303 天前

关于 @ddd-ts-kit/validation 的常见问题

@ddd-ts-kit/validation 是什么?

一个用于输入验证的模式契约,带有用于zod、类验证器、TypeBox和标准模式的适配器。

如何安装 @ddd-ts-kit/validation?

在项目目录运行 npm install @ddd-ts-kit/validation,或使用 pnpm add / yarn add / bun add。

@ddd-ts-kit/validation 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ddd-ts-kit/validation 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ddd-ts-kit/validation 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ddd-ts-kit/validation 可以商用吗?

@ddd-ts-kit/validation 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ddd-ts-kit/validation 还在维护吗?

最新版本 0.106.0 发布于 2026/11/01(2 天前);至今共发布 63 个版本,其中过去 12 个月发布了 63 个;首次发布于 2026/10/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。