json-schema-faker
Version:
Generate valid JSON data from JSON Schema definitions
1,475 lines (1,462 loc) • 93.1 kB
JavaScript
#!/usr/bin/env node
// src/random.ts
function mulberry32(seed) {
let s = seed | 0;
return () => {
s = s + 1831565813 | 0;
let t = Math.imul(s ^ s >>> 15, 1 | s);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function createRandom(seed) {
const raw = mulberry32(seed);
let callCount = 0;
const random = {
next() {
callCount++;
return raw();
},
int(min, max) {
return min + Math.floor(random.next() * (max - min + 1));
},
bool(probability = 0.5) {
return random.next() < probability;
},
pick(arr) {
return arr[random.int(0, arr.length - 1)];
},
shuffle(arr) {
const result = [...arr];
for (let i = result.length - 1;i > 0; i--) {
const j = random.int(0, i);
[result[i], result[j]] = [result[j], result[i]];
}
return result;
},
fork() {
const newSeed = Math.floor(random.next() * 4294967296);
return createRandom(newSeed);
}
};
return random;
}
// src/generators/null.ts
function generateNull(_ctx) {
return null;
}
// src/generators/boolean.ts
function generateBoolean(ctx) {
return ctx.random.bool();
}
// src/generators/number.ts
function generateNumber(schema, ctx, asInteger = false) {
let min = -1000;
let max = 1000;
let exclusiveMin = false;
let exclusiveMax = false;
if (schema.minimum !== undefined)
min = schema.minimum;
if (schema.maximum !== undefined)
max = schema.maximum;
if (schema.exclusiveMinimum !== undefined) {
min = schema.exclusiveMinimum;
exclusiveMin = true;
}
if (schema.exclusiveMaximum !== undefined) {
max = schema.exclusiveMaximum;
exclusiveMax = true;
}
if (schema.multipleOf !== undefined) {
return generateMultipleOf(schema.multipleOf, min, max, exclusiveMin, exclusiveMax, ctx);
}
if (asInteger) {
const lo2 = exclusiveMin ? Math.floor(min) + 1 : Math.ceil(min);
const hi2 = exclusiveMax ? Math.ceil(max) - 1 : Math.floor(max);
return ctx.random.int(lo2, hi2);
}
const lo = exclusiveMin ? min + Number.EPSILON : min;
const hi = exclusiveMax ? max - Number.EPSILON : max;
return lo + ctx.random.next() * (hi - lo);
}
function generateMultipleOf(multipleOf, min, max, exclusiveMin, exclusiveMax, ctx) {
let lo = Math.ceil(min / multipleOf);
let hi = Math.floor(max / multipleOf);
if (exclusiveMin && lo * multipleOf === min)
lo++;
if (exclusiveMax && hi * multipleOf === max)
hi--;
if (lo > hi)
return lo * multipleOf;
const factor = ctx.random.int(lo, hi);
return factor * multipleOf;
}
// src/generators/integer.ts
function generateInteger(schema, ctx) {
if (schema.autoIncrement) {
const initialOffset = schema.initialOffset ?? 1;
const counterKey = getAutoIncrementCounterKey(ctx.path);
const counters = ctx.autoIncrementCounters ?? new Map;
const currentCount = counters.get(counterKey) ?? initialOffset - 1;
const nextValue = currentCount + 1;
counters.set(counterKey, nextValue);
if (ctx.autoIncrementCounters) {
ctx.autoIncrementCounters.set(counterKey, nextValue);
}
return nextValue;
}
return generateNumber(schema, ctx, true);
}
function getAutoIncrementCounterKey(path) {
return path.split("/").filter((segment) => segment !== "" && !/^\d+$/.test(segment)).join("/");
}
// src/pattern/regex-gen.ts
var WORD_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
var DIGIT_CHARS = "0123456789";
var SPACE_CHARS = `
`;
var PRINTABLE = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#%&*()-_=+[]{}|;:',.<>/? ";
function generateFromRegex(pattern, random) {
const ast = parse(pattern, 0);
return generate(ast.node, random);
}
function generate(node, random) {
switch (node.type) {
case "literal":
return node.value;
case "dot":
return random.pick([...PRINTABLE]);
case "anchor":
return "";
case "charClass":
return generateCharClass(node, random);
case "sequence":
return node.nodes.map((n) => generate(n, random)).join("");
case "alternation":
return generate(random.pick(node.branches), random);
case "quantifier": {
const count = random.int(node.min, Math.min(node.max, node.min + 5));
return Array.from({ length: count }, () => generate(node.node, random)).join("");
}
case "group":
return generate(node.node, random);
}
}
function generateCharClass(node, random) {
if (!node.negated) {
return random.pick(node.chars);
}
const excluded = new Set(node.chars);
const allowed = [...PRINTABLE].filter((c) => !excluded.has(c));
return allowed.length > 0 ? random.pick(allowed) : "a";
}
function parse(pattern, pos) {
const branches = [];
let current = [];
while (pos < pattern.length) {
const ch = pattern[pos];
if (ch === ")")
break;
if (ch === "|") {
branches.push(seq(current));
current = [];
pos++;
continue;
}
if (ch === "(") {
let groupStart = pos + 1;
if (pattern[groupStart] === "?") {
if (pattern[groupStart + 1] === ":") {
groupStart += 2;
} else if (pattern[groupStart + 1] === "=" || pattern[groupStart + 1] === "!") {
let depth = 1;
let p = groupStart;
while (p < pattern.length && depth > 0) {
p++;
if (pattern[p] === "(")
depth++;
if (pattern[p] === ")")
depth--;
}
pos = p + 1;
continue;
} else {
groupStart += 2;
while (groupStart < pattern.length && pattern[groupStart] !== ">")
groupStart++;
groupStart++;
}
}
const inner = parse(pattern, groupStart);
const groupNode = { type: "group", node: inner.node };
pos = inner.pos + 1;
const qResult2 = tryQuantifier(pattern, pos, groupNode);
current.push(qResult2.node);
pos = qResult2.pos;
continue;
}
if (ch === "[") {
const ccResult = parseCharClass(pattern, pos);
const qResult2 = tryQuantifier(pattern, ccResult.pos, ccResult.node);
current.push(qResult2.node);
pos = qResult2.pos;
continue;
}
if (ch === ".") {
const dotNode = { type: "dot" };
pos++;
const qResult2 = tryQuantifier(pattern, pos, dotNode);
current.push(qResult2.node);
pos = qResult2.pos;
continue;
}
if (ch === "^" || ch === "$") {
current.push({ type: "anchor" });
pos++;
continue;
}
if (ch === "\\") {
pos++;
const escaped = parseEscape(pattern, pos);
pos = escaped.pos;
const qResult2 = tryQuantifier(pattern, pos, escaped.node);
current.push(qResult2.node);
pos = qResult2.pos;
continue;
}
const litNode = { type: "literal", value: ch };
pos++;
const qResult = tryQuantifier(pattern, pos, litNode);
current.push(qResult.node);
pos = qResult.pos;
}
branches.push(seq(current));
const node = branches.length === 1 ? branches[0] : { type: "alternation", branches };
return { node, pos };
}
function seq(nodes) {
if (nodes.length === 0)
return { type: "literal", value: "" };
if (nodes.length === 1)
return nodes[0];
return { type: "sequence", nodes };
}
function parseEscape(pattern, pos) {
const ch = pattern[pos];
pos++;
switch (ch) {
case "d":
return { node: { type: "charClass", chars: [...DIGIT_CHARS], negated: false }, pos };
case "D":
return { node: { type: "charClass", chars: [...DIGIT_CHARS], negated: true }, pos };
case "w":
return { node: { type: "charClass", chars: [...WORD_CHARS], negated: false }, pos };
case "W":
return { node: { type: "charClass", chars: [...WORD_CHARS], negated: true }, pos };
case "s":
return { node: { type: "charClass", chars: [...SPACE_CHARS], negated: false }, pos };
case "S":
return { node: { type: "charClass", chars: [...SPACE_CHARS], negated: true }, pos };
case "b":
case "B":
return { node: { type: "anchor" }, pos };
default:
return { node: { type: "literal", value: ch }, pos };
}
}
function parseCharClass(pattern, pos) {
pos++;
let negated = false;
if (pattern[pos] === "^") {
negated = true;
pos++;
}
const chars = [];
while (pos < pattern.length && pattern[pos] !== "]") {
if (pattern[pos] === "\\" && pos + 1 < pattern.length) {
pos++;
const ch = pattern[pos];
switch (ch) {
case "d":
chars.push(...DIGIT_CHARS);
break;
case "w":
chars.push(...WORD_CHARS);
break;
case "s":
chars.push(...SPACE_CHARS);
break;
default:
chars.push(ch);
}
pos++;
continue;
}
if (pos + 2 < pattern.length && pattern[pos + 1] === "-" && pattern[pos + 2] !== "]") {
const start = pattern.charCodeAt(pos);
const end = pattern.charCodeAt(pos + 2);
for (let c = start;c <= end; c++) {
chars.push(String.fromCharCode(c));
}
pos += 3;
continue;
}
chars.push(pattern[pos]);
pos++;
}
pos++;
return { node: { type: "charClass", chars, negated }, pos };
}
function tryQuantifier(pattern, pos, node) {
if (pos >= pattern.length)
return { node, pos };
const ch = pattern[pos];
let min;
let max;
switch (ch) {
case "*":
min = 0;
max = 5;
pos++;
break;
case "+":
min = 1;
max = 5;
pos++;
break;
case "?":
min = 0;
max = 1;
pos++;
break;
case "{": {
const result = parseBraceQuantifier(pattern, pos);
if (result) {
min = result.min;
max = result.max;
pos = result.pos;
} else {
return { node, pos };
}
break;
}
default:
return { node, pos };
}
if (pos < pattern.length && (pattern[pos] === "?" || pattern[pos] === "+")) {
pos++;
}
return { node: { type: "quantifier", node, min, max }, pos };
}
function parseBraceQuantifier(pattern, pos) {
const match = pattern.slice(pos).match(/^\{(\d+)(?:,(\d*))?\}/);
if (!match)
return null;
const min = parseInt(match[1], 10);
let max;
if (match[2] === undefined) {
max = min;
} else if (match[2] === "") {
max = min + 5;
} else {
max = parseInt(match[2], 10);
}
return { min, max, pos: pos + match[0].length };
}
// src/utils/helpers.ts
function pad2(n) {
return n < 10 ? `0${n}` : `${n}`;
}
function daysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
// src/generators/string.ts
var DEFAULT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
function canPatternMatchNonEmpty(pattern) {
if (pattern === "^" || pattern === "$" || pattern === "^$") {
return false;
}
return true;
}
function generateString(schema, ctx) {
if (schema.format) {
if (schema.format === "date-time" && (ctx.minDateTime !== undefined || ctx.maxDateTime !== undefined)) {
return generateDateTimeWithRange(ctx);
}
const formatGen = ctx.formatRegistry.get(schema.format);
if (formatGen) {
const result2 = formatGen(ctx.random);
const effectiveMinLength = ctx.minLength ?? schema.minLength;
const effectiveMaxLength = ctx.maxLength ?? schema.maxLength;
if (effectiveMinLength !== undefined && result2.length < effectiveMinLength) {
return padString(result2, effectiveMinLength, ctx);
}
if (effectiveMaxLength !== undefined && result2.length > effectiveMaxLength) {
return result2.slice(0, effectiveMaxLength);
}
return result2;
}
}
if (schema.pattern) {
const effectiveMinLength = ctx.minLength ?? schema.minLength;
const effectiveMaxLength = ctx.maxLength ?? schema.maxLength;
const maxAttempts = 50;
for (let attempt = 0;attempt < maxAttempts; attempt++) {
const result3 = generateFromRegex(schema.pattern, ctx.random);
if (effectiveMinLength !== undefined && result3.length < effectiveMinLength) {
continue;
}
if (effectiveMaxLength !== undefined && result3.length > effectiveMaxLength) {
continue;
}
return result3;
}
const result2 = generateFromRegex(schema.pattern, ctx.random);
const patternCanMatchNonEmpty = canPatternMatchNonEmpty(schema.pattern);
if (effectiveMinLength !== undefined && result2.length < effectiveMinLength) {
if (patternCanMatchNonEmpty) {
return padString(result2, effectiveMinLength, ctx);
}
}
if (effectiveMaxLength !== undefined && result2.length > effectiveMaxLength) {
return result2.slice(0, effectiveMaxLength);
}
return result2;
}
const minLen = schema.minLength ?? 0;
const maxLen = schema.maxLength ?? Math.max(minLen + 10, 10);
const length = ctx.random.int(minLen, maxLen);
let result = "";
for (let i = 0;i < length; i++) {
result += ctx.random.pick([...DEFAULT_CHARS]);
}
if (ctx.maxLength !== undefined && result.length > ctx.maxLength) {
result = result.slice(0, ctx.maxLength);
}
if (ctx.minLength !== undefined && result.length < ctx.minLength) {
result = padString(result, ctx.minLength, ctx);
}
return result;
}
function padString(str, targetLength, ctx) {
while (str.length < targetLength) {
str += ctx.random.pick([...DEFAULT_CHARS]);
}
return str;
}
function generateDateTimeWithRange(ctx) {
const minDt = ctx.minDateTime ? new Date(ctx.minDateTime) : new Date("1970-01-01");
const maxDt = ctx.maxDateTime ? new Date(ctx.maxDateTime) : new Date;
const minTime = minDt.getTime();
const maxTime = maxDt.getTime();
const randomTime = minTime + ctx.random.next() * (maxTime - minTime);
const date = new Date(randomTime);
const year = date.getFullYear();
const month = pad2(date.getMonth() + 1);
const day = pad2(date.getDate());
return `${year}-${month}-${day}T01:01:01.0Z`;
}
// src/generators/enum-const.ts
function generateEnumConst(schema, ctx) {
if (schema.const !== undefined) {
return structuredClone(schema.const);
}
if (schema.enum !== undefined && schema.enum.length > 0) {
let validValues = schema.enum;
if (schema.minimum !== undefined || schema.maximum !== undefined || schema.exclusiveMinimum !== undefined || schema.exclusiveMaximum !== undefined || schema.multipleOf !== undefined) {
const min = schema.exclusiveMinimum ?? schema.minimum ?? -Infinity;
const max = schema.exclusiveMaximum ?? schema.maximum ?? Infinity;
const isExclusiveMin = schema.exclusiveMinimum !== undefined;
const isExclusiveMax = schema.exclusiveMaximum !== undefined;
validValues = validValues.filter((v) => {
if (typeof v !== "number")
return true;
const aboveMin = isExclusiveMin ? v > min : v >= min;
const belowMax = isExclusiveMax ? v < max : v <= max;
if (!aboveMin || !belowMax)
return false;
if (schema.multipleOf !== undefined) {
const multipleOf = schema.multipleOf;
const decimalPlaces = multipleOf.toString().includes(".") ? multipleOf.toString().split(".")[1].length : 0;
const precision = Math.pow(10, decimalPlaces);
const scaledVal = Math.round(v * precision);
const scaledMultipleOf = Math.round(multipleOf * precision);
if (scaledVal % scaledMultipleOf !== 0)
return false;
}
return true;
});
}
if (schema.minLength !== undefined || schema.maxLength !== undefined) {
const minLen = schema.minLength ?? 0;
const maxLen = schema.maxLength ?? Infinity;
validValues = validValues.filter((v) => {
if (typeof v !== "string")
return true;
return v.length >= minLen && v.length <= maxLen;
});
}
if (schema.minItems !== undefined || schema.maxItems !== undefined) {
const minItems = schema.minItems ?? 0;
const maxItems = schema.maxItems ?? Infinity;
validValues = validValues.filter((v) => {
if (!Array.isArray(v))
return true;
return v.length >= minItems && v.length <= maxItems;
});
}
if (schema.minProperties !== undefined || schema.maxProperties !== undefined) {
const minProps = schema.minProperties ?? 0;
const maxProps = schema.maxProperties ?? Infinity;
validValues = validValues.filter((v) => {
if (typeof v !== "object" || v === null || Array.isArray(v))
return true;
return Object.keys(v).length >= minProps && Object.keys(v).length <= maxProps;
});
}
if (validValues.length === 0) {
const constraints = [];
if (schema.minLength !== undefined)
constraints.push(`minLength: ${schema.minLength}`);
if (schema.maxLength !== undefined)
constraints.push(`maxLength: ${schema.maxLength}`);
if (schema.minItems !== undefined)
constraints.push(`minItems: ${schema.minItems}`);
if (schema.maxItems !== undefined)
constraints.push(`maxItems: ${schema.maxItems}`);
if (schema.minProperties !== undefined)
constraints.push(`minProperties: ${schema.minProperties}`);
if (schema.maxProperties !== undefined)
constraints.push(`maxProperties: ${schema.maxProperties}`);
if (schema.minimum !== undefined)
constraints.push(`minimum: ${schema.minimum}`);
if (schema.maximum !== undefined)
constraints.push(`maximum: ${schema.maximum}`);
if (schema.exclusiveMinimum !== undefined)
constraints.push(`exclusiveMinimum: ${schema.exclusiveMinimum}`);
if (schema.exclusiveMaximum !== undefined)
constraints.push(`exclusiveMaximum: ${schema.exclusiveMaximum}`);
if (schema.multipleOf !== undefined)
constraints.push(`multipleOf: ${schema.multipleOf}`);
throw new Error(`enum values conflict with constraints [${constraints.join(", ")}] at ${ctx.path}`);
}
const value = ctx.random.pick(validValues);
return typeof value === "object" && value !== null ? structuredClone(value) : value;
}
throw new Error(`generateEnumConst called without enum or const at ${ctx.path}`);
}
// src/merge.ts
function mergeSchemas(schemas) {
const result = {};
for (const schema of schemas) {
if (typeof schema === "boolean") {
if (!schema)
return { not: {} };
continue;
}
mergePairInto(result, schema);
}
return result;
}
function mergePair(a, b) {
if (typeof a === "boolean" || typeof b === "boolean") {
return {};
}
if (typeof a !== "object" || a === null) {
return typeof b === "object" && b !== null ? b : {};
}
if (typeof b !== "object" || b === null) {
return a;
}
const result = { ...a };
mergePairInto(result, b);
return result;
}
function mergePairInto(target, source) {
if (source.type !== undefined) {
if (target.type === undefined) {
target.type = source.type;
} else {
target.type = intersectTypes(target.type, source.type);
}
}
if (source.minimum !== undefined) {
target.minimum = target.minimum !== undefined ? Math.max(target.minimum, source.minimum) : source.minimum;
}
if (source.maximum !== undefined) {
target.maximum = target.maximum !== undefined ? Math.min(target.maximum, source.maximum) : source.maximum;
}
if (source.exclusiveMinimum !== undefined) {
target.exclusiveMinimum = target.exclusiveMinimum !== undefined ? Math.max(target.exclusiveMinimum, source.exclusiveMinimum) : source.exclusiveMinimum;
}
if (source.exclusiveMaximum !== undefined) {
target.exclusiveMaximum = target.exclusiveMaximum !== undefined ? Math.min(target.exclusiveMaximum, source.exclusiveMaximum) : source.exclusiveMaximum;
}
if (source.multipleOf !== undefined) {
target.multipleOf = source.multipleOf;
}
if (source.minLength !== undefined) {
target.minLength = target.minLength !== undefined ? Math.max(target.minLength, source.minLength) : source.minLength;
}
if (source.maxLength !== undefined) {
target.maxLength = target.maxLength !== undefined ? Math.min(target.maxLength, source.maxLength) : source.maxLength;
}
if (source.pattern !== undefined) {
target.pattern = source.pattern;
}
if (source.format !== undefined) {
target.format = source.format;
}
if (source.minItems !== undefined) {
target.minItems = target.minItems !== undefined ? Math.max(target.minItems, source.minItems) : source.minItems;
}
if (source.maxItems !== undefined) {
target.maxItems = target.maxItems !== undefined ? Math.min(target.maxItems, source.maxItems) : source.maxItems;
}
if (source.uniqueItems)
target.uniqueItems = true;
if (source.items !== undefined)
target.items = source.items;
if (source.prefixItems !== undefined) {
if (target.prefixItems === undefined) {
target.prefixItems = source.prefixItems;
} else {
const merged = [];
const maxLen = Math.max(target.prefixItems.length, source.prefixItems.length);
for (let i = 0;i < maxLen; i++) {
const targetItem = target.prefixItems[i];
const sourceItem = source.prefixItems[i];
if (targetItem !== undefined && sourceItem !== undefined) {
merged.push(mergePair(targetItem, sourceItem));
} else if (targetItem !== undefined) {
merged.push(targetItem);
} else if (sourceItem !== undefined) {
merged.push(sourceItem);
}
}
target.prefixItems = merged;
}
}
if (source.contains !== undefined) {
if (target.containsAll !== undefined) {
target.containsAll = [...target.containsAll, source.contains];
} else if (target.contains !== undefined) {
target.containsAll = [target.contains, source.contains];
delete target.contains;
} else {
target.contains = source.contains;
}
}
if (source.properties) {
target.properties = deepMergeProperties(target.properties ?? {}, source.properties);
}
if (source.required) {
target.required = [...new Set([...target.required ?? [], ...source.required])];
}
if (source.additionalProperties !== undefined) {
target.additionalProperties = source.additionalProperties;
}
if (source.patternProperties) {
target.patternProperties = { ...target.patternProperties ?? {}, ...source.patternProperties };
}
if (source.minProperties !== undefined) {
target.minProperties = target.minProperties !== undefined ? Math.max(target.minProperties, source.minProperties) : source.minProperties;
}
if (source.maxProperties !== undefined) {
target.maxProperties = target.maxProperties !== undefined ? Math.min(target.maxProperties, source.maxProperties) : source.maxProperties;
}
if (source.const !== undefined)
target.const = source.const;
if (source.enum !== undefined) {
if (target.enum !== undefined) {
const sourceSet = new Set(source.enum.map((v) => JSON.stringify(v)));
target.enum = target.enum.filter((v) => sourceSet.has(JSON.stringify(v)));
} else {
target.enum = source.enum;
}
}
if (source.$ref)
target.$ref = source.$ref;
if (source.$defs)
target.$defs = { ...target.$defs ?? {}, ...source.$defs };
if (source.allOf) {
target.allOf = [...target.allOf ?? [], ...source.allOf];
}
if (source.anyOf)
target.anyOf = source.anyOf;
if (source.oneOf)
target.oneOf = source.oneOf;
}
function intersectTypes(a, b) {
const aArr = Array.isArray(a) ? a : [a];
const bArr = Array.isArray(b) ? b : [b];
const intersection = aArr.filter((t) => bArr.includes(t));
if (intersection.length === 0)
return aArr[0];
if (intersection.length === 1)
return intersection[0];
return intersection;
}
function deepMergeProperties(target, source) {
const result = { ...target };
for (const [key, sourceSchema] of Object.entries(source)) {
const targetSchema = target[key];
if (targetSchema === undefined) {
result[key] = sourceSchema;
} else if (typeof targetSchema === "object" && typeof sourceSchema === "object") {
result[key] = mergePair(targetSchema, sourceSchema);
} else {
result[key] = sourceSchema;
}
}
return result;
}
// src/utils/schema-keywords.ts
var SCHEMA_KEYWORDS = new Set([
"$schema",
"$id",
"$ref",
"$defs",
"$anchor",
"$dynamicRef",
"$dynamicAnchor",
"$vocabulary",
"$comment",
"type",
"enum",
"const",
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
"minLength",
"maxLength",
"pattern",
"format",
"contentEncoding",
"contentMediaType",
"contentSchema",
"items",
"prefixItems",
"contains",
"minItems",
"maxItems",
"uniqueItems",
"minContains",
"maxContains",
"properties",
"required",
"additionalProperties",
"patternProperties",
"minProperties",
"maxProperties",
"propertyNames",
"dependentRequired",
"dependentSchemas",
"allOf",
"anyOf",
"oneOf",
"not",
"if",
"then",
"else",
"faker",
"chance",
"jsonPath",
"template",
"default",
"examples",
"description",
"title",
"readOnly",
"writeOnly",
"deprecated"
]);
function isJsonSchema(value) {
if (typeof value !== "object" || value === null) {
return false;
}
const obj = value;
return Object.keys(obj).some((key) => SCHEMA_KEYWORDS.has(key));
}
// src/generators/object.ts
function matchesConstraint(value, constraint) {
if (typeof constraint !== "object" || constraint === null) {
return false;
}
const constraintObj = constraint;
if (constraintObj.const !== undefined) {
return value === constraintObj.const;
}
if (constraintObj.enum !== undefined) {
return constraintObj.enum.some((v) => v === value);
}
return false;
}
function isImpossibleSchema(schema) {
if (typeof schema === "boolean") {
return !schema;
}
if (typeof schema !== "object" || schema === null) {
return false;
}
if (schema.not === true || typeof schema.not === "object" && schema.not !== null && Object.keys(schema.not).length === 0) {
return true;
}
return false;
}
function getInferredProperties(schema) {
if (schema.properties) {
return schema.properties;
}
if (schema.type === "object" || schema.type === undefined) {
const inferred = {};
let hasInferredProperties = false;
for (const [key, value] of Object.entries(schema)) {
if (SCHEMA_KEYWORDS.has(key)) {
continue;
}
if (isJsonSchema(value)) {
inferred[key] = value;
hasInferredProperties = true;
} else {
inferred[key] = { const: value };
hasInferredProperties = true;
}
}
if (hasInferredProperties) {
return inferred;
}
}
return {};
}
function getInferredRequired(schema, inferredProperties) {
if (schema.required) {
return schema.required;
}
if (schema.properties === undefined && Object.keys(inferredProperties).length > 0) {
return Object.keys(inferredProperties);
}
return [];
}
async function generateObject(schema, ctx) {
const inferredProperties = getInferredProperties(schema);
const inferredRequired = getInferredRequired(schema, inferredProperties);
if (ctx.depth >= ctx.maxDepth) {
if (inferredRequired.length === 0) {
return {};
}
const result2 = {};
for (const key of inferredRequired) {
const propSchema = inferredProperties[key];
if (propSchema && typeof propSchema === "object" && propSchema !== null) {
const schemaObj = propSchema;
if (schemaObj.const !== undefined) {
result2[key] = schemaObj.const;
} else if (schemaObj.default !== undefined) {
result2[key] = schemaObj.default;
} else if (schemaObj.enum !== undefined && schemaObj.enum.length > 0) {
result2[key] = schemaObj.enum[0];
} else {
result2[key] = generateStubValue(schemaObj.type);
}
} else {
result2[key] = null;
}
}
return result2;
}
if (schema.additionalProperties === false) {
const hasProperties = Object.keys(inferredProperties).length > 0;
const hasPatternProperties = schema.patternProperties && Object.keys(schema.patternProperties).length > 0;
const definedKeys2 = new Set(Object.keys(inferredProperties));
const required2 = new Set(inferredRequired);
const requiredKeys = [...required2].filter((k) => !definedKeys2.has(k));
if ((schema.minProperties ?? 0) > 0 && !hasProperties && !hasPatternProperties) {
throw new Error(`missing properties for ${schema.minProperties} in ${ctx.path}: additionalProperties is false but no properties or patternProperties defined`);
}
if (requiredKeys.length > 0 && !hasPatternProperties && !hasProperties) {
throw new Error(`missing properties for ${requiredKeys.join(", ")} in ${ctx.path}: additionalProperties is false but required properties not defined in properties`);
}
}
const childCtxPath = ctx.path === "/" ? "/properties" : `${ctx.path}/properties`;
const childCtx = { ...ctx, depth: ctx.depth + 1, path: childCtxPath };
const result = {};
const definedKeys = new Set;
const required = new Set(inferredRequired);
const alwaysFakeOptionals = ctx.alwaysFakeOptionals ?? false;
const fillProperties = ctx.fillProperties ?? true;
const useFixedProbabilities = ctx.fixedProbabilities ?? false;
const optionalsProbability = ctx.optionalsProbability ?? 0.5;
const optionalKeys = Object.keys(inferredProperties).filter((key) => !required.has(key));
let propertiesToInclude;
if (useFixedProbabilities && !alwaysFakeOptionals && optionalKeys.length > 0) {
const targetCount = Math.round(optionalKeys.length * optionalsProbability);
propertiesToInclude = new Set(optionalKeys.slice(0, targetCount));
}
const shouldGenerateOptional = (key) => {
if (alwaysFakeOptionals)
return true;
if (propertiesToInclude)
return propertiesToInclude.has(key);
return ctx.random.bool(optionalsProbability);
};
if (Object.keys(inferredProperties).length > 0) {
for (const [key, propSchema] of Object.entries(inferredProperties)) {
if (isImpossibleSchema(propSchema)) {
continue;
}
definedKeys.add(key);
const propCtx = createPropertyContext(childCtx, key);
const isRequired = required.has(key);
const isObjectType = typeof propSchema === "object" && propSchema !== null && propSchema.type === "object";
let mergedPropSchema = propSchema;
if (schema.patternProperties && typeof propSchema === "object" && propSchema !== null) {
const matchingPatterns = [];
for (const [pattern, patSchema] of Object.entries(schema.patternProperties)) {
try {
if (new RegExp(pattern).test(key)) {
matchingPatterns.push(patSchema);
}
} catch {}
}
if (matchingPatterns.length > 0) {
mergedPropSchema = mergeSchemas([propSchema, ...matchingPatterns]);
}
}
if (isRequired) {
const value = await walk(mergedPropSchema, propCtx);
if (value !== undefined) {
result[key] = value;
}
} else if (shouldGenerateOptional(key)) {
const value = await walk(mergedPropSchema, propCtx);
if (value !== undefined) {
result[key] = value;
}
} else if (isObjectType && !fillProperties) {
const nestedRequired = propSchema.required ?? [];
if (nestedRequired.length > 0) {
const nestedResult = {};
for (const nestedKey of nestedRequired) {
const nestedSchema = (propSchema.properties ?? {})[nestedKey] ?? {};
const nestedCtx = createPropertyContext(propCtx, nestedKey);
const nestedValue = await walk(nestedSchema, nestedCtx);
if (nestedValue !== undefined) {
nestedResult[nestedKey] = nestedValue;
}
}
result[key] = nestedResult;
}
}
}
}
for (const key of required) {
if (result[key] !== undefined || definedKeys.has(key))
continue;
if (schema.patternProperties) {
const matchingSchemas = [];
for (const [pattern, patSchema] of Object.entries(schema.patternProperties)) {
if (new RegExp(pattern).test(key)) {
matchingSchemas.push(patSchema);
}
}
if (matchingSchemas.length > 0) {
const mergedSchema = matchingSchemas.length === 1 ? matchingSchemas[0] : mergeSchemas(matchingSchemas);
const propCtx = createPropertyContext(childCtx, key);
const value = await walk(mergedSchema, propCtx);
if (value !== undefined) {
result[key] = value;
}
definedKeys.add(key);
}
}
if (!definedKeys.has(key)) {
const addlSchema = schema.additionalProperties ?? true;
if (addlSchema !== false) {
const propCtx = createPropertyContext(childCtx, key);
const value = await walk(addlSchema === true ? {} : addlSchema, propCtx);
if (value !== undefined) {
result[key] = value;
}
definedKeys.add(key);
}
}
}
if (schema.dependencies) {
for (const [propName, dependency] of Object.entries(schema.dependencies)) {
if (typeof dependency !== "object" || dependency === null || Array.isArray(dependency)) {
continue;
}
if (result[propName] !== undefined && !definedKeys.has(propName + "_dep")) {
definedKeys.add(propName + "_dep");
const branches = dependency.oneOf ?? dependency.anyOf ?? dependency.allOf ?? [dependency];
let matchingBranch = null;
for (const branch of branches) {
if (typeof branch !== "object" || branch === null)
continue;
const branchProps = branch.properties;
const branchNot = branch.not;
if (branchProps && branchProps[propName]) {
const constraint = branchProps[propName];
const generatedValue = result[propName];
if (matchesConstraint(generatedValue, constraint)) {
matchingBranch = branch;
break;
}
}
if (branchNot?.properties) {
const notPropConstraints = branchNot.properties[propName];
if (notPropConstraints) {
const generatedValue = result[propName];
if (!matchesConstraint(generatedValue, notPropConstraints)) {
matchingBranch = branch;
break;
}
}
}
}
if (!matchingBranch && branches.length > 0 && typeof branches[0] === "object") {
matchingBranch = branches[0];
}
if (matchingBranch?.not) {
const notSchema = matchingBranch.not;
if (notSchema.required && Array.isArray(notSchema.required)) {
for (const forbiddenProp of notSchema.required) {
delete result[forbiddenProp];
}
}
}
if (matchingBranch?.required) {
for (const reqProp of matchingBranch.required) {
if (reqProp === propName)
continue;
if (result[reqProp] !== undefined)
continue;
const reqPropSchema = matchingBranch.properties?.[reqProp] ?? { type: "object" };
const reqPropCtx = createPropertyContext(childCtx, reqProp);
const value = await walk(reqPropSchema, reqPropCtx);
if (value !== undefined) {
result[reqProp] = value;
}
}
}
if (matchingBranch && matchingBranch.properties) {
for (const [depPropName, depPropSchema] of Object.entries(matchingBranch.properties)) {
if (depPropName === propName)
continue;
const depPropCtx = createPropertyContext(childCtx, depPropName);
const value = await walk(depPropSchema, depPropCtx);
if (value !== undefined) {
result[depPropName] = value;
}
}
}
}
}
}
const minProps = schema.minProperties ?? 0;
const maxProps = schema.maxProperties;
if (Object.keys(result).length < minProps) {
if (schema.patternProperties && Object.keys(schema.patternProperties).length > 0) {
const patterns = Object.entries(schema.patternProperties);
let attempts = 0;
const maxAttempts = 1000;
while (Object.keys(result).length < minProps && attempts < maxAttempts) {
attempts++;
const [pattern, patSchema] = ctx.random.pick(patterns);
const key = generateKeyMatchingPattern(pattern, ctx);
if (key && !result[key]) {
const propCtx = createPropertyContext(childCtx, key);
const value = await walk(patSchema, propCtx);
if (value !== undefined) {
result[key] = value;
}
definedKeys.add(key);
}
}
}
if (Object.keys(result).length < minProps) {
const addlSchema = schema.additionalProperties;
const genSchema = addlSchema === undefined || addlSchema === true ? { type: "string" } : addlSchema;
if (genSchema !== false) {
let attempts = 0;
const maxAttempts = 1000;
while (Object.keys(result).length < minProps && attempts < maxAttempts) {
attempts++;
const key = `prop${ctx.random.int(0, 99999)}`;
if (!result[key]) {
const propCtx = createPropertyContext(childCtx, key);
const value = await walk(genSchema, propCtx);
if (value !== undefined) {
result[key] = value;
}
definedKeys.add(key);
}
}
}
}
if (Object.keys(result).length < minProps) {
const definedPropsList = Object.keys(inferredProperties).join(", ");
throw new Error(`properties '${definedPropsList}' were not found while additionalProperties is false in ${ctx.path}`);
}
}
if (maxProps !== undefined && Object.keys(result).length > maxProps) {
const keys = Object.keys(result);
const toRemove = keys.filter((k) => !required.has(k));
ctx.random.shuffle(toRemove);
while (Object.keys(result).length > maxProps && toRemove.length > 0) {
const key = toRemove.pop();
delete result[key];
}
}
if (ctx.pruneProperties && ctx.pruneProperties.length > 0) {
pruneObjectProperties(result, ctx.pruneProperties);
}
resolveTemplates(result, inferredProperties);
return result;
}
function createPropertyContext(ctx, key) {
const encodedKey = encodeJsonPointerSegment(key);
const outputPath = ctx.outputPath === "/" ? `/${encodedKey}` : `${ctx.outputPath}/${encodedKey}`;
return {
...ctx,
path: `${ctx.path}/${key}`,
outputPath
};
}
function encodeJsonPointerSegment(segment) {
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
}
function resolveTemplates(result, properties) {
for (const [key, propSchema] of Object.entries(properties)) {
if (typeof propSchema !== "object" || propSchema === null)
continue;
const schema = propSchema;
if (schema.template === undefined)
continue;
const template = schema.template;
if (typeof template !== "string")
continue;
const resolved = template.replace(/#\{(\w+)\}/g, (_, propName) => {
const value = result[propName];
return value !== undefined ? String(value) : "";
});
result[key] = resolved;
}
}
function generateKeyMatchingPattern(pattern, ctx) {
const prefixMatch = pattern.match(/^\^([a-zA-Z]+)(\.\*|\(\?\:.*\))?/);
if (prefixMatch) {
const prefix = prefixMatch[1];
const suffix = prefixMatch[2];
if (suffix === ".*" || suffix?.includes("?:")) {
const suffixLen = ctx.random.int(0, 10);
const chars2 = "abcdefghijklmnopqrstuvwxyz";
let randomSuffix = "";
for (let i = 0;i < suffixLen; i++) {
randomSuffix += ctx.random.pick(chars2.split(""));
}
return prefix + randomSuffix;
}
return prefix;
}
const suffixMatch = pattern.match(/(.*\*)?([a-zA-Z]+)\$$/);
if (suffixMatch) {
const suffix = suffixMatch[2];
const prefixLen = ctx.random.int(1, 10);
const chars2 = "abcdefghijklmnopqrstuvwxyz";
let randomPrefix = "";
for (let i = 0;i < prefixLen; i++) {
randomPrefix += ctx.random.pick(chars2.split(""));
}
return randomPrefix + suffix;
}
const exactMatch = pattern.match(/^\^([a-zA-Z]+)\$$/);
if (exactMatch) {
return exactMatch[1];
}
const len = ctx.random.int(3, 10);
const chars = "abcdefghijklmnopqrstuvwxyz";
let key = "";
for (let i = 0;i < len; i++) {
key += ctx.random.pick(chars.split(""));
}
try {
if (new RegExp(pattern).test(key)) {
return key;
}
} catch {}
return null;
}
function pruneObjectProperties(obj, propertiesToPrune) {
if (typeof obj !== "object" || obj === null) {
return;
}
if (Array.isArray(obj)) {
for (const item of obj) {
pruneObjectProperties(item, propertiesToPrune);
}
return;
}
const record = obj;
for (const key of Object.keys(record)) {
if (propertiesToPrune.includes(key)) {
delete record[key];
} else {
pruneObjectProperties(record[key], propertiesToPrune);
}
}
}
function generateStubValue(type) {
const resolvedType = Array.isArray(type) ? type[0] : type;
switch (resolvedType) {
case "string":
return "";
case "number":
case "integer":
return 0;
case "boolean":
return false;
case "null":
return null;
case "array":
return [];
case "object":
return {};
default:
return null;
}
}
// src/generators/array.ts
async function generateArray(schema, ctx) {
if (ctx.depth >= ctx.maxDepth) {
const needed = schema.minItems ?? 0;
return needed > 0 ? Array.from({ length: needed }, () => null) : [];
}
if (ctx.refDepthReached) {
const needed = schema.minItems ?? 0;
return needed > 0 ? Array.from({ length: needed }, () => null) : [];
}
if (schema.items === false && (schema.minItems ?? 0) > 0) {
const prefixItemCount = schema.prefixItems?.length ?? 0;
if ((schema.minItems ?? 0) > prefixItemCount) {
throw new Error(`missing items for ${schema.minItems} in ${ctx.path}: items is false but minItems (${schema.minItems}) exceeds prefixItems count (${prefixItemCount})`);
}
}
const childCtxPath = ctx.path === "/" ? "/items" : `${ctx.path}/items`;
const childCtx = { ...ctx, depth: ctx.depth + 1, path: childCtxPath };
const result = [];
const maxRefDepth = ctx.refDepthMax;
let minItems = ctx.minItems ?? schema.minItems ?? 0;
let maxItems = ctx.maxItems ?? schema.maxItems ?? Math.max(minItems, ctx.maxDefaultItems);
if (schema.maxItems !== undefined && maxItems > schema.maxItems) {
maxItems = schema.maxItems;
}
if (schema.minItems !== undefined && minItems < schema.minItems) {
minItems = schema.minItems;
}
if (maxItems < minItems) {
if (schema.maxItems !== undefined) {
minItems = maxItems;
} else {
maxItems = minItems;
}
}
if (ctx.alwaysFakeOptionals) {
maxItems = Math.max(minItems, maxItems);
}
const seen = schema.uniqueItems ? new Set : null;
const addItem = async (itemSchema, itemCtx) => {
let item = await walk(itemSchema, itemCtx);
if (item === undefined) {
return "omitted";
}
if (seen) {
const key = JSON.stringify(item);
if (seen.has(key)) {
for (let attempts = 0;attempts < 50; attempts++) {
item = await walk(itemSchema, createItemContext(childCtx, result.length));
if (item === undefined) {
continue;
}
const newKey = JSON.stringify(item);
if (!seen.has(newKey)) {
seen.add(newKey);
result.push(item);
return "added";
}
}
return "failed";
}
seen.add(key);
}
result.push(item);
return "added";
};
if (schema.prefixItems) {
for (let i = 0;i < schema.prefixItems.length && result.length < maxItems; i++) {
const itemCtx = createItemContext(childCtx, i);
const status = await addItem(schema.prefixItems[i], itemCtx);
if (status === "failed")
break;
}
}
let targetLen;
if (maxRefDepth !== undefined && ctx.refDepth >= maxRefDepth - 1) {
const needed = schema.minItems ?? 0;
return needed > 0 ? Array.from({ length: needed }, () => null) : [];
}
const alwaysFakeOptionals = ctx.alwaysFakeOptionals ?? false;
const useFixedProbabilities = ctx.fixedProbabilities ?? false;
const optionalsProbability = ctx.optionalsProbability ?? 0.5;
if (alwaysFakeOptionals) {
targetLen = maxItems;
} else if (useFixedProbabilities) {
const availableRange = maxItems - Math.max(minItems, result.length);
const additionalItems = Math.round(availableRange * optionalsProbability);
targetLen = Math.max(minItems, result.length) + additionalItems;
} else {
targetLen = ctx.random.int(Math.max(minItems, result.length), maxItems);
}
const hasExplicitItems = schema.items !== undefined;
const hasPrefixItems = schema.prefixItems !== undefined;
const hasAdditionalItems = schema.additionalItems !== undefined;
let additionalItemSchema;
if (hasPrefixItems) {
if (hasAdditionalItems) {
additionalItemSchema = schema.additionalItems;
} else if (schema.items !== undefined) {
additionalItemSchema = schema.items;
}
} else {
additionalItemSchema = schema.items;
}
if (additionalItemSchema !== false && additionalItemSchema !== undefined) {
const itemSchema = additionalItemSchema ?? {};
let attempts = 0;
const maxAttempts = Math.max(targetLen * 20, 50);
while (result.length < targetLen) {
const status = await addItem(itemSchema, createItemContext(childCtx, result.length));
if (status === "added") {
attempts = 0;
continue;
}
attempts++;
if (status === "failed" || attempts >= maxAttempts)
break;
}
}
if (schema.contains) {
const minContains = schema.minContains ?? 1;
const maxContains = schema.maxContains ?? Math.max(minContains, 1);
const containsCount = ctx.random.int(minContains, maxContains);
for (let i = 0;i < containsCount; i++) {
let item = await walk(schema.contains, createItemContext(childCtx, result.length));
if (item === undefined) {
continue;
}
if (seen) {
const key = JSON.stringify(item);
if (seen.has(key)) {
let uniqueFound = false;
for (let attempts = 0;attempts < 50; attempts++) {
item = await walk(schema.contains, createItemContext(childCtx, result.length));
if (item === undefined) {
continue;
}
const newKey = JSON.stringify(item);
if (!seen.has(newKey)) {
seen.add(newKey);
uniqueFound = true;
break;
}
}
if (!uniqueFound)
continue;
} else {
seen.add(key);
}
}
if (result.length < maxItems) {
const pos = ctx.random.int(0, result.length);
result.splice(pos, 0, item);
} else if (result.length > 0) {
const pos = ctx.random.int(0, result.length - 1);
result[pos] = item;
}
}
while (result.length > maxItems) {
result.pop();
}
}
if (schema.containsAll) {
const claimedPositions = new Set;
for (const containsSchema of schema.containsAll) {
let item = await walk(containsSchema, createItemContext(childCtx, result.length));
if (item === undefined) {
continue;
}
if (seen) {
const key = JSON.stringify(item);
if (seen.has(key)) {
let uniqueFound = false;
for (let attempts = 0;attempts < 50; attempts++) {
item = await walk(containsSchema, createItemContext(childCtx, result.length));
if (item === undefined) {
continue;
}
const newKey = JSON.stringify(item);
if (!seen.has(newKey)) {
seen.add(newKey);
uniqueFound = true;
break;
}
}
if (!uniqueFound)
continue;
} else {
seen.add(key);
}
}
if (result.length < maxItems) {
const pos = ctx.random.int(0, result.length);
result.splice(pos, 0, item);
const shifted = new Set;
for (const p of claimedPositions) {
shifted.add(p >= pos ? p + 1 : p);
}
claimedPositions.clear();
for (const p of shifted)
claimedPositions.add(p);
claimedPositions.add(pos);
} else if (result.length > 0) {
const available = Array.from({ length: result.length }, (_, i) => i).filter((i) => !claimedPositions.has(i));
if (available.length > 0) {
const pos = available[ctx.random.int(0, available.length - 1)];
result[pos] = item;
claimedPositions.add(pos);
} else {
result.push(item);
claimedPositions.add(result.length - 1);
}
}
}
while (result.length > maxItems) {
result.pop();
}
}
if (schema.uniqueItems && result.length > 0) {
return fillUniqueItems(result, schema, childCtx, minItems, maxItems, seen);
}
return result;
}
async function fillUniqueItems(arr, schema, ctx, minItems, maxItems, seen) {
const itemSchema = schema.items ?? {};
let attempts = 0;
const maxAttempts = 100;
while (arr.length < minItems && attempts < maxAttempts) {
attempts++;
const item = await walk(itemSchema, createItemContext(ctx, arr.length));
if (item ==