Minimal JSON Schema Validator
A dependency free validator for the common JSON Schema subset that reports every violation with its JSON path instead of stopping at the first mismatch.
Code
export interface ValidationIssue {
readonly path: string;
readonly message: string;
}
export type ValidationResult =
| { readonly valid: true }
| { readonly valid: false; readonly issues: ValidationIssue[] };
type Schema = {
readonly type?: "object" | "array" | "string" | "number" | "boolean" | "null";
readonly required?: readonly string[];
readonly properties?: Record<string, Schema>;
readonly items?: Schema;
readonly minLength?: number;
readonly minimum?: number;
readonly enum?: readonly unknown[];
};
function typeOf(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
function childPath(path: string, segment: string | number): string {
// 数组下标用方括号,属性用点号——贴近常见 JSON path 书写习惯
if (typeof segment === "number") return path + "[" + segment + "]";
return path === "$" ? "$." + segment : path + "." + segment;
}
export function validate(value: unknown, schema: Schema, path = "$"): ValidationResult {
const issues: ValidationIssue[] = [];
walk(value, schema, path, issues);
return issues.length === 0 ? { valid: true } : { valid: false, issues };
}
function walk(value: unknown, schema: Schema, path: string, issues: ValidationIssue[]): void {
if (schema.type && typeOf(value) !== schema.type) {
issues.push({
path,
message: "expected " + schema.type + " but got " + typeOf(value),
});
return;
}
if (schema.enum && !schema.enum.includes(value)) {
issues.push({ path, message: "value must be one of " + JSON.stringify(schema.enum) });
}
if (schema.type === "string" && typeof value === "string") {
if (schema.minLength !== undefined && value.length < schema.minLength) {
issues.push({ path, message: "string shorter than " + schema.minLength + " characters" });
}
}
if (schema.type === "number" && typeof value === "number") {
if (schema.minimum !== undefined && value < schema.minimum) {
issues.push({ path, message: "number below minimum " + schema.minimum });
}
}
if (schema.type === "object" && value && typeof value === "object" && !Array.isArray(value)) {
const record = value as Record<string, unknown>;
for (const key of schema.required ?? []) {
if (!(key in record)) {
issues.push({ path, message: "missing required property " + key });
}
}
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
if (key in record) walk(record[key], childSchema, childPath(path, key), issues);
}
}
if (schema.type === "array" && Array.isArray(value)) {
if (schema.items) {
value.forEach((item, index) => walk(item, schema.items as Schema, childPath(path, index), issues));
}
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.