export class SchemaValidator { /** * Validates data against a JSON schema. * @returns null if valid, or a string describing the error if invalid. */ static validate(schema: any, data: any): string | null { if (!schema) return null; // Handle cases where schema is just a string (name of type) const type = typeof schema === 'string' ? schema : schema.type; if (type === 'object') { if (typeof data !== 'object' || data === null || Array.isArray(data)) { return `Expected object, got ${typeof data}`; } const properties = schema.properties || {}; const required = schema.required || []; // Check required properties for (const req of required) { if (!(req in data)) { return `Missing required property: ${req}`; } } // Recursively validate properties for (const key in data) { if (properties[key]) { const error = this.validate(properties[key], data[key]); if (error) return `${key}: ${error}`; } } } else if (type === 'array') { if (!Array.isArray(data)) { return `Expected array, got ${typeof data}`; } if (schema.items) { for (let i = 0; i < data.length; i++) { const error = this.validate(schema.items, data[i]); if (error) return `item[${i}]: ${error}`; } } } else if (type === 'string') { if (typeof data !== 'string') return `Expected string, got ${typeof data}`; if (schema.enum && !schema.enum.includes(data)) { return `Value "${data}" not in enum [${schema.enum.join(', ')}]`; } } else if (type === 'number' || type === 'integer') { if (typeof data !== 'number') return `Expected number, got ${typeof data}`; } else if (type === 'boolean') { if (typeof data !== 'boolean') return `Expected boolean, got ${typeof data}`; } return null; } }