API 开发中最令人头疼的环节之一,不是写接口文档,而是准备测试数据。当你的 JSON Schema 定义了 30 个字段、嵌套 5 层、包含 8 种 oneOf 分支时,手写 Mock 数据不仅效率低下,还极易遗漏边界场景。据统计,开发团队平均花费 15-20% 的 API 开发时间在构造测试数据上,而这些数据往往在 Schema 变更后就彻底失效。本文将从零构建一个能自动解析 JSON Schema 并生成逼真测试数据的引擎,覆盖 $ref 引用解析、组合关键字处理、类型推断等核心难点,并与 json-schema-faker、@faker-js/faker 等主流工具做深度对比。
🔧 一、核心架构:Schema 解析与数据生成管线
构建 Mock 数据引擎的核心思路是:将 JSON Schema 视为一棵 AST(抽象语法树),递归遍历每个节点,根据类型约束和格式提示生成符合要求的数据。整个管线分为三个阶段:Schema 预处理、类型解析、数据生成。
1.1 JSON Schema 关键字速查
在实现之前,先厘清 JSON Schema 中影响数据生成的核心关键字:
| 关键字 | 作用 | 对生成器的影响 | 示例 |
|---|---|---|---|
type |
约束值类型 | 决定生成哪种数据 | "type": "string" |
enum |
枚举值列表 | 从中随机选取 | "enum": ["A","B","C"] |
const |
固定常量 | 直接返回该值 | "const": "fixed" |
minimum / maximum |
数值范围 | 限定生成区间 | "minimum": 1, "maximum": 100 |
minLength / maxLength |
字符串长度 | 控制生成长度 | "minLength": 5 |
pattern |
正则约束 | 用 randexp 生成 | "pattern": "^[A-Z]{3}$" |
format |
格式提示 | 映射到 faker 方法 | "format": "email" |
$ref |
引用其他 Schema | 递归解析引用 | "$ref": "#/defs/User" |
oneOf / anyOf |
组合约束 | 随机选取一个分支 | 详见下文 |
allOf |
合并约束 | 深度合并后生成 | 详见下文 |
📌 记住:
enum和const的优先级最高。当它们出现时,不需要考虑其他约束,直接使用即可。
1.2 最小可行实现:类型到数据的映射
先实现一个最基础的生成器,能处理简单类型:
// json-mock-generator.ts — 最小可行的 JSON Schema Mock 数据生成器
type Schema = Record<string, any>;
// 基础随机工具函数
const randomInt = (min: number, max: number) =>
Math.floor(Math.random() * (max - min + 1)) + min;
const randomString = (length: number) => {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
return Array.from({ length }, () => chars[randomInt(0, chars.length - 1)]).join('');
};
// 根据 type 生成基础数据
function generateByType(type: string, schema: Schema): any {
switch (type) {
case 'string':
if (schema.enum) return schema.enum[randomInt(0, schema.enum.length - 1)];
if (schema.const) return schema.const;
return randomString(schema.minLength ?? 8);
case 'number':
case 'integer':
if (schema.enum) return schema.enum[randomInt(0, schema.enum.length - 1)];
const min = schema.minimum ?? 0;
const max = schema.maximum ?? 1000;
const val = Math.random() * (max - min) + min;
return type === 'integer' ? Math.floor(val) : parseFloat(val.toFixed(2));
case 'boolean':
return Math.random() > 0.5;
case 'null':
return null;
case 'array':
const len = schema.minItems ?? randomInt(1, schema.maxItems ?? 3);
const items = schema.items ?? { type: 'string' };
return Array.from({ length: len }, () => generateFromSchema(items));
case 'object':
return generateObject(schema);
default:
return null;
}
}
// 生成对象类型数据
function generateObject(schema: Schema): Record<string, any> {
const result: Record<string, any> = {};
const properties = schema.properties ?? {};
const required = new Set(schema.required ?? []);
for (const [key, propSchema] of Object.entries(properties)) {
// 非必填字段有 70% 概率生成
if (!required.has(key) && Math.random() > 0.7) continue;
result[key] = generateFromSchema(propSchema as Schema);
}
return result;
}
// 主入口:递归生成
function generateFromSchema(schema: Schema): any {
if (schema.const) return schema.const;
if (schema.enum) return schema.enum[randomInt(0, schema.enum.length - 1)];
if (schema.type) return generateByType(schema.type, schema);
// 无 type 时尝试推断
if (schema.properties) return generateObject(schema);
if (schema.items) return generateByType('array', schema);
return null;
}
export { generateFromSchema };
这个 60 行的生成器已经能处理大部分简单 Schema,但面对真实世界的 API 定义还远远不够。接下来我们逐个攻克核心难点。
🚀 二、三大核心难点:$ref、组合关键字与格式生成
2.1 $ref 引用解析:构建定义注册表
真实世界的 JSON Schema 几乎都会使用 $ref 引用复用类型定义。比如一个用户 API 可能这样定义:
{
"type": "object",
"properties": {
"user": { "$ref": "#/definitions/User" },
"orders": {
"type": "array",
"items": { "$ref": "#/definitions/Order" }
}
},
"definitions": {
"User": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "email"]
},
"Order": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"total": { "type": "number", "minimum": 0 }
}
}
}
}
⚠️ **警告:**不处理
$ref就直接生成数据,会导致生成的 Mock 数据完全不符合预期——引用字段会变成null,破坏整个测试用例。
实现 $ref 解析需要构建一个注册表(Registry),将所有定义路径映射到对应的 Schema 对象:
// ref-resolver.ts — $ref 引用解析器
type SchemaMap = Map<string, Schema>;
// 构建 Schema 注册表:将所有 definitions/$defs 展平为路径->Schema 映射
function buildRegistry(schema: Schema, rootPath = '#'): SchemaMap {
const registry: SchemaMap = new Map();
function walk(obj: any, path: string) {
if (!obj || typeof obj !== 'object') return;
registry.set(path, obj);
// 处理 definitions 和 $defs(JSON Schema 2019-09+)
for (const keyword of ['definitions', '$defs']) {
if (obj[keyword]) {
for (const [name, subSchema] of Object.entries(obj[keyword])) {
walk(subSchema, `${path}/${keyword}/${name}`);
}
}
}
// 递归处理 properties、items、allOf/anyOf/oneOf 中的子 Schema
if (obj.properties) {
for (const [key, prop] of Object.entries(obj.properties)) {
walk(prop, `${path}/properties/${key}`);
}
}
if (obj.items && typeof obj.items === 'object') {
walk(obj.items, `${path}/items`);
}
for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
if (Array.isArray(obj[keyword])) {
obj[keyword].forEach((sub: any, i: number) => {
walk(sub, `${path}/${keyword}/${i}`);
});
}
}
}
walk(schema, rootPath);
return registry;
}
// 解析 $ref 指向的实际 Schema
function resolveRef(ref: string, registry: SchemaMap): Schema {
const resolved = registry.get(ref);
if (!resolved) {
console.warn(`⚠️ 无法解析 $ref: ${ref},使用空对象兜底`);
return {};
}
// 处理链式引用:A -> B -> C
if (resolved.$ref) {
return resolveRef(resolved.$ref, registry);
}
return resolved;
}
💡 **提示:**生产级生成器还需要处理远程
$ref(如"$ref": "https://example.com/schema.json#/User"),这需要先 fetch 远程 Schema 再注册。本文聚焦本地场景,远程引用留给读者扩展。
2.2 oneOf / anyOf / allOf 组合处理
组合关键字是 Mock 数据生成中最复杂的部分,三种关键字的行为完全不同:
allOf(合并):将多个 Schema 深度合并为一个,所有约束都必须满足。处理策略是将所有子 Schema 的 properties、required、type 等合并:
// combination.ts — 组合关键字处理器
function mergeSchemas(schemas: Schema[]): Schema {
const merged: Schema = {};
for (const sub of schemas) {
// 合并 properties
if (sub.properties) {
merged.properties = { ...merged.properties, ...sub.properties };
}
// 合并 required(取并集)
if (sub.required) {
merged.required = [...new Set([...(merged.required ?? []), ...sub.required])];
}
// 合并 type(取交集,通常 allOf 中 type 一致)
if (sub.type) merged.type = sub.type;
// 合并 minimum/maximum(取更严格的约束)
if (sub.minimum !== undefined) {
merged.minimum = Math.max(merged.minimum ?? -Infinity, sub.minimum);
}
if (sub.maximum !== undefined) {
merged.maximum = Math.min(merged.maximum ?? Infinity, sub.maximum);
}
}
return merged;
}
oneOf(互斥选取):恰好匹配一个分支。生成器随机选取一个分支生成数据。但要注意:如果分支之间有 const 或 enum 区分,应该优先选取能生成有效数据的分支:
function handleOneOf(oneOf: Schema[], registry: SchemaMap): any {
// 解析所有 $ref
const resolved = oneOf.map(s =>
s.$ref ? resolveRef(s.$ref, registry) : s
);
// 如果有 const/enum 分支,优先选取
const constBranches = resolved.filter(s => s.const !== undefined);
if (constBranches.length > 0) {
const chosen = constBranches[randomInt(0, constBranches.length - 1)];
return chosen.const;
}
// 随机选取一个分支
const chosen = resolved[randomInt(0, resolved.length - 1)];
return generateFromSchema(chosen);
}
anyOf(至少匹配一个):行为类似 oneOf,但允许匹配多个。Mock 数据生成时通常简化为随机选取一个分支,因为单个分支生成的数据已经满足至少一个条件。
⚠️ 警告:
oneOf在真实 API 中常被滥用。当分支之间没有明确的区分字段(discriminator)时,JSON Schema 验证器需要逐个尝试分支,这对 Mock 生成器也是一大挑战。建议始终在oneOf中添加const或required字段作为区分标识。
2.3 format 到 Faker 的映射:生成逼真数据
JSON Schema 的 format 关键字是提升 Mock 数据质量的关键。将 format 映射到 @faker-js/faker 的方法,可以让生成的数据从「随机乱码」变成「看起来像真的」:
// format-mapper.ts — format 到 Faker 的映射层
import { faker } from '@faker-js/faker';
const formatGenerators: Record<string, () => any> = {
'email': () => faker.internet.email(),
'uri': () => faker.internet.url(),
'hostname': () => faker.internet.domainName(),
'ipv4': () => faker.internet.ipv4(),
'ipv6': () => faker.internet.ipv6(),
'date': () => faker.date.recent().toISOString().split('T')[0],
'date-time': () => faker.date.recent().toISOString(),
'time': () => faker.date.recent().toISOString().split('T')[1],
'uuid': () => faker.string.uuid(),
'phone': () => faker.phone.number(),
'color': () => faker.color.rgb(),
'credit-card': () => faker.finance.creditCardNumber(),
'iban': () => faker.finance.iban(),
'regex': () => '', // 需要配合 randexp 库
'password': () => faker.internet.password(),
'username': () => faker.internet.userName(),
'slug': () => faker.helpers.slugify(faker.lorem.words(3)),
};
function generateByFormat(format: string): any {
const generator = formatGenerators[format];
if (generator) return generator();
// 未知 format 回退到默认字符串
return randomString(10);
}
⚡ 关键结论:format 映射是区分「玩具级」和「生产级」Mock 生成器的分水岭。没有 Faker 集成的生成器只能产出 abc123 这样的垃圾数据,而集成后能产出 zhang.wei@example.com 这样的逼真数据。
📊 三、完整引擎实现与工具对比
3.1 整合所有模块
将前面的模块整合为一个完整的生成引擎:
// mock-engine.ts — 完整的 JSON Schema Mock 数据生成引擎
import { faker } from '@faker-js/faker';
type Schema = Record<string, any>;
type SchemaMap = Map<string, Schema>;
interface GeneratorOptions {
/** 是否为所有非必填字段也生成数据,默认 false */
alwaysIncludeOptional?: boolean;
/** 数组默认长度,默认 1-3 随机 */
arrayLength?: { min: number; max: number };
/** 自定义 format 生成器 */
customFormats?: Record<string, () => any>;
/** 随机种子(用于可重复生成) */
seed?: number;
}
class MockEngine {
private registry: SchemaMap = new Map();
private options: Required<GeneratorOptions>;
constructor(private rootSchema: Schema, options: GeneratorOptions = {}) {
this.options = {
alwaysIncludeOptional: options.alwaysIncludeOptional ?? false,
arrayLength: options.arrayLength ?? { min: 1, max: 3 },
customFormats: options.customFormats ?? {},
seed: options.seed ?? Date.now(),
};
this.registry = this.buildRegistry(rootSchema);
}
private buildRegistry(schema: Schema, path = '#'): SchemaMap {
const map: SchemaMap = new Map();
const walk = (obj: any, p: string) => {
if (!obj || typeof obj !== 'object') return;
map.set(p, obj);
for (const kw of ['definitions', '$defs']) {
if (obj[kw]) {
for (const [n, s] of Object.entries(obj[kw])) {
walk(s, `${p}/${kw}/${n}`);
}
}
}
if (obj.properties) {
for (const [k, v] of Object.entries(obj.properties)) {
walk(v, `${p}/properties/${k}`);
}
}
if (obj.items && typeof obj.items === 'object') walk(obj.items, `${p}/items`);
for (const kw of ['allOf', 'anyOf', 'oneOf']) {
if (Array.isArray(obj[kw])) {
obj[kw].forEach((s: any, i: number) => walk(s, `${p}/${kw}/${i}`));
}
}
};
walk(schema, path);
return map;
}
private resolve(ref: string): Schema {
const s = this.registry.get(ref);
if (!s) return {};
return s.$ref ? this.resolve(s.$ref) : s;
}
generate(schema?: Schema): any {
return this._gen(schema ?? this.rootSchema);
}
private _gen(schema: Schema): any {
// 1. const / enum 优先
if (schema.const !== undefined) return schema.const;
if (schema.enum) return schema.enum[Math.floor(Math.random() * schema.enum.length)];
// 2. $ref 解析
if (schema.$ref) return this._gen(this.resolve(schema.$ref));
// 3. allOf 合并
if (schema.allOf) {
const merged = schema.allOf.reduce((acc: Schema, sub: Schema) => {
const resolved = sub.$ref ? this.resolve(sub.$ref) : sub;
return { ...acc, ...resolved, properties: { ...acc.properties, ...resolved.properties } };
}, {});
return this._gen(merged);
}
// 4. oneOf / anyOf 随机选取
if (schema.oneOf || schema.anyOf) {
const branches = (schema.oneOf ?? schema.anyOf).map((s: Schema) =>
s.$ref ? this.resolve(s.$ref) : s
);
return this._gen(branches[Math.floor(Math.random() * branches.length)]);
}
// 5. format 生成
if (schema.format) return this.generateFormat(schema.format, schema);
// 6. pattern 生成(需要 randexp)
if (schema.pattern) return this.generateFromPattern(schema.pattern, schema);
// 7. 按 type 生成
return this.generateByType(schema.type ?? 'object', schema);
}
private generateFormat(format: string, schema: Schema): any {
if (this.options.customFormats[format]) return this.options.customFormats[format]();
const map: Record<string, () => any> = {
'email': () => faker.internet.email(),
'uri': () => faker.internet.url(),
'uuid': () => faker.string.uuid(),
'date': () => faker.date.recent().toISOString().split('T')[0],
'date-time': () => faker.date.recent().toISOString(),
'ipv4': () => faker.internet.ipv4(),
'phone': () => faker.phone.number(),
};
return (map[format] ?? (() => randomString(10)))();
}
private generateFromPattern(pattern: string, schema: Schema): string {
// 简化实现:生产环境建议集成 randexp 库
return `[pattern:${pattern}]`;
}
private generateByType(type: string, schema: Schema): any {
switch (type) {
case 'string': return randomString(schema.minLength ?? 8);
case 'number': {
const min = schema.minimum ?? 0, max = schema.maximum ?? 1000;
return parseFloat((Math.random() * (max - min) + min).toFixed(2));
}
case 'integer': {
const min = schema.minimum ?? 0, max = schema.maximum ?? 1000;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
case 'boolean': return Math.random() > 0.5;
case 'null': return null;
case 'array': {
const { min, max } = this.options.arrayLength;
const len = schema.minItems ?? Math.floor(Math.random() * (max - min + 1)) + min;
const items = schema.items ?? { type: 'string' };
return Array.from({ length: len }, () => this._gen(items));
}
case 'object':
default:
return this.generateObject(schema);
}
}
private generateObject(schema: Schema): Record<string, any> {
const result: Record<string, any> = {};
const props = schema.properties ?? {};
const required = new Set(schema.required ?? []);
for (const [key, propSchema] of Object.entries(props)) {
if (!required.has(key) && !this.options.alwaysIncludeOptional && Math.random() > 0.7) {
continue;
}
result[key] = this._gen(propSchema as Schema);
}
return result;
}
}
// 辅助函数
function randomString(len: number): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
export { MockEngine, type GeneratorOptions };
3.2 使用示例
// usage.ts — 演示 MockEngine 的完整用法
import { MockEngine } from './mock-engine';
const apiSchema = {
type: 'object',
required: ['id', 'name', 'email', 'role'],
properties: {
id: { type: 'integer', minimum: 1 },
name: { type: 'string', minLength: 2, maxLength: 20 },
email: { type: 'string', format: 'email' },
role: { enum: ['admin', 'editor', 'viewer'] },
avatar: { type: 'string', format: 'uri' },
tags: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 5 },
address: {
type: 'object',
properties: {
city: { type: 'string' },
zipCode: { type: 'string', pattern: '^[0-9]{6}$' }
}
},
status: {
oneOf: [
{ type: 'object', properties: { type: { const: 'active' }, lastLogin: { type: 'string', format: 'date-time' } }, required: ['type'] },
{ type: 'object', properties: { type: { const: 'banned' }, reason: { type: 'string' } }, required: ['type'] }
]
}
}
};
const engine = new MockEngine(apiSchema, { alwaysIncludeOptional: true });
// 生成单条数据
const user = engine.generate();
console.log(JSON.stringify(user, null, 2));
// 生成批量数据
const users = Array.from({ length: 10 }, () => engine.generate());
console.table(users.map(u => ({ id: u.id, name: u.name, email: u.email, role: u.role })));
输出示例:
{
"id": 42,
"name": "k8x9p2m",
"email": "john.doe@example.com",
"role": "editor",
"avatar": "https://example.com/avatar.png",
"tags": ["x7k", "m3p"],
"address": {
"city": "a9b2c4d6e8",
"zipCode": "100086"
},
"status": {
"type": "active",
"lastLogin": "2026-06-10T14:30:22.000Z"
}
}
3.3 主流工具深度对比
市面上已有几款 JSON Schema Mock 数据工具,它们各有侧重:
| 特性 | 本文 MockEngine | json-schema-faker | MSW + Faker | 手写 Fixture |
|---|---|---|---|---|
| Schema 解析深度 | ✅ 完整 | ✅ 完整 | ❌ 需手写映射 | ❌ 完全手写 |
| $ref 支持 | ✅ 本地+链式 | ✅ 远程也支持 | ❌ 无 | ❌ 无 |
| oneOf/anyOf | ✅ 随机选取 | ✅ 随机选取 | ❌ 手动判断 | ❌ 手动判断 |
| format 映射 | ✅ Faker 集成 | ⚠️ 部分支持 | ✅ 完整 | ✅ 完整 |
| TypeScript 类型 | ✅ 原生 | ❌ JS 为主 | ✅ | ✅ |
| 可定制性 | ✅ 高 | ⚠️ 中 | ✅ 高 | ✅ 最高 |
| 包大小 | ~15KB | ~80KB | ~200KB+ | 0KB |
| 维护状态(2026) | 本文代码 | ⚠️ 更新缓慢 | ✅ 活跃 | N/A |
💡 提示:
json-schema-faker曾是最流行的方案,但其维护频率在 2025 年后明显下降,且对 JSON Schema 2020-12 的支持不完整。如果你的项目使用较新的 Schema 版本,建议基于本文代码自行实现或使用 MSW + Faker 组合。
⚠️ 四、生产环境避坑指南
在将 Mock 数据引擎接入真实项目时,以下几个坑点需要特别注意:
4.1 递归引用导致无限循环
{
"definitions": {
"TreeNode": {
"type": "object",
"properties": {
"value": { "type": "string" },
"children": {
"type": "array",
"items": { "$ref": "#/definitions/TreeNode" }
}
}
}
}
}
❌ **避免:**直接递归生成
TreeNode会导致栈溢出。必须加入深度限制。
解决方案:
// safe-generator.ts — 递归深度限制
const MAX_DEPTH = 5;
function safeGenerate(schema: Schema, depth = 0): any {
if (depth >= MAX_DEPTH) {
// 达到深度上限,返回占位值
return schema.type === 'array' ? [] : {};
}
return generateWithDepth(schema, depth + 1);
}
4.2 数值边界陷阱
Schema 定义了 "minimum": 0, "maximum": 0 时,生成器必须返回 0 而不是随机数。"exclusiveMinimum": 0 意味着 0 本身不合法,需要生成 > 0 的值。这些边界条件在测试中经常被忽略。
⚠️ 警告:
exclusiveMinimum在 JSON Schema Draft 4 中是布尔值(true表示排除),在 Draft 6+ 中是数值(表示排除的边界值)。如果你的生成器需要兼容多个版本,务必分别处理。
4.3 嵌套 allOf 的合并顺序
当 allOf 中的子 Schema 对同一字段定义了不同约束时,合并顺序会影响结果。正确的做法是:后出现的约束覆盖先出现的,但 minimum/maximum 取更严格的值。
// ❌ 错误写法:简单的 Object.assign 会丢失数值约束的合并逻辑
const merged = Object.assign({}, ...allOfSchemas);
// ✅ 正确写法:对特殊字段做深度合并
function deepMergeAllOf(schemas: Schema[]): Schema {
return schemas.reduce((acc, cur) => {
const result = { ...acc, ...cur };
// minimum 取较大值(更严格)
if (acc.minimum !== undefined && cur.minimum !== undefined) {
result.minimum = Math.max(acc.minimum, cur.minimum);
}
// maximum 取较小值(更严格)
if (acc.maximum !== undefined && cur.maximum !== undefined) {
result.maximum = Math.min(acc.maximum, cur.maximum);
}
// required 取并集
if (acc.required || cur.required) {
result.required = [...new Set([...(acc.required ?? []), ...(cur.required ?? [])])];
}
return result;
}, {});
}
4.4 性能优化:Schema 缓存
对于大型 Schema(如 OpenAPI 3.1 完整定义,可能包含数百个端点),每次生成都重新构建注册表是不可接受的。将解析后的 Schema 注册表缓存起来,只在 Schema 变更时重建:
// cached-engine.ts — 带缓存的生成引擎
class CachedMockEngine {
private cache = new Map<string, any>();
private registryHash: string;
constructor(schema: Schema) {
// 对 Schema 内容做哈希,检测变更
this.registryHash = this.hashSchema(schema);
if (!this.cache.has(this.registryHash)) {
this.cache.set(this.registryHash, buildRegistry(schema));
}
}
private hashSchema(schema: Schema): string {
// 简化实现,生产环境建议用 crypto.subtle.digest
return JSON.stringify(schema).length.toString(36);
}
}
💡 五、进阶:可重复生成与快照测试
Mock 数据的一个高级需求是可重复生成——相同的种子(seed)产出相同的数据。这对于快照测试(Snapshot Testing)和回归测试至关重要。
// seeded-random.ts — 基于种子的可重复随机数生成器
class SeededRandom {
private state: number;
constructor(seed: number) {
this.state = seed;
}
// Mulberry32 算法:简单高效的 32 位 PRNG
next(): number {
this.state |= 0;
this.state = (this.state + 0x6D2B79F5) | 0;
let t = Math.imul(this.state ^ (this.state >>> 15), 1 | this.state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
// 生成指定范围的整数
int(min: number, max: number): number {
return Math.floor(this.next() * (max - min + 1)) + min;
}
// 从数组中随机选取
pick<T>(arr: T[]): T {
return arr[this.int(0, arr.length - 1)];
}
}
// 用法
const rng = new SeededRandom(42);
console.log(rng.int(1, 100)); // 每次运行 seed=42 都输出相同结果
console.log(rng.pick(['a', 'b', 'c']));
将 SeededRandom 集成到 MockEngine 中,就可以在 CI/CD 流水线中保证每次生成的 Mock 数据完全一致,避免因随机数据导致的测试波动。
📌 **记住:**可重复生成不仅用于测试,还可以用于「数据回放」场景——在 Bug 复现时用相同种子重建完全一致的测试数据。
✅ 总结与工具推荐
构建一个生产级的 JSON Schema Mock 数据引擎,核心挑战不在于随机数据生成本身,而在于正确解析和处理 JSON Schema 的各种关键字组合。关键要点回顾:
- ✅ 必须处理
$ref:真实 Schema 几乎都有引用,不处理等于废了 - ✅
allOf要深度合并:不能简单 Object.assign,数值约束需要特殊处理 - ✅
format要映射到 Faker:这是 Mock 数据从「垃圾」到「逼真」的分水岭 - ✅ 递归引用要加深度限制:否则一个 TreeNode 就能让你的程序崩溃
- ✅ 用种子实现可重复生成:CI/CD 流水线中测试稳定性依赖于此
- ❌ 不要忽略
exclusiveMinimum/exclusiveMaximum:边界值是常见的 Bug 来源 - ❌ 不要假设所有 Schema 都有
type:有些 Schema 只用properties隐式表示对象
推荐工具链:
| 工具 | 用途 | 推荐度 |
|---|---|---|
@faker-js/faker |
逼真数据生成 | ⭐⭐⭐⭐⭐ |
json-schema-faker |
开箱即用的 Schema Mock | ⭐⭐⭐ |
ajv |
Schema 验证(校验生成的数据) | ⭐⭐⭐⭐⭐ |
msw |
API Mock 拦截 | ⭐⭐⭐⭐ |
randexp |
正则表达式数据生成 | ⭐⭐⭐⭐ |
最终建议:如果你的 API Schema 相对简单且不频繁变更,json-schema-faker 是最快的方案。但如果你需要深度定制、支持最新 Schema 版本、或集成到自研的 API 平台中,基于本文的架构自行实现是更可持续的选择。