Update schema validation to throw errors
This commit is contained in:
parent
36e641e001
commit
964db74108
@ -93,10 +93,7 @@ class JsonSchemaProxyHandler {
|
|||||||
|
|
||||||
value = JsonSchema.isolate(value);
|
value = JsonSchema.isolate(value);
|
||||||
|
|
||||||
const error = JsonSchemaProxyHandler.validate(value, propertySchema);
|
JsonSchemaProxyHandler.validate(value, propertySchema);
|
||||||
if (error !== null) {
|
|
||||||
throw new Error(`Invalid value: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
target[property] = value;
|
target[property] = value;
|
||||||
return true;
|
return true;
|
||||||
@ -193,186 +190,175 @@ class JsonSchemaProxyHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static validate(value, schema) {
|
static validate(value, schema) {
|
||||||
let result = JsonSchemaProxyHandler.validateSingleSchema(value, schema);
|
JsonSchemaProxyHandler.validateSingleSchema(value, schema);
|
||||||
if (result !== null) { return result; }
|
JsonSchemaProxyHandler.validateConditional(value, schema);
|
||||||
|
JsonSchemaProxyHandler.validateAllOf(value, schema);
|
||||||
result = JsonSchemaProxyHandler.validateConditional(value, schema);
|
JsonSchemaProxyHandler.validateAnyOf(value, schema);
|
||||||
if (result !== null) { return result; }
|
JsonSchemaProxyHandler.validateOneOf(value, schema);
|
||||||
|
JsonSchemaProxyHandler.validateNoneOf(value, schema);
|
||||||
result = JsonSchemaProxyHandler.validateAllOf(value, schema);
|
|
||||||
if (result !== null) { return result; }
|
|
||||||
|
|
||||||
result = JsonSchemaProxyHandler.validateAnyOf(value, schema);
|
|
||||||
if (result !== null) { return result; }
|
|
||||||
|
|
||||||
result = JsonSchemaProxyHandler.validateOneOf(value, schema);
|
|
||||||
if (result !== null) { return result; }
|
|
||||||
|
|
||||||
result = JsonSchemaProxyHandler.validateNoneOf(value, schema);
|
|
||||||
if (result !== null) { return result; }
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateConditional(value, schema) {
|
static validateConditional(value, schema) {
|
||||||
const ifCondition = schema.if;
|
const ifCondition = schema.if;
|
||||||
if (!JsonSchemaProxyHandler.isObject(ifCondition)) { return null; }
|
if (!JsonSchemaProxyHandler.isObject(ifCondition)) { return; }
|
||||||
|
|
||||||
const thenSchema = schema.then;
|
const thenSchema = schema.then;
|
||||||
if (JsonSchemaProxyHandler.isObject(thenSchema)) {
|
if (JsonSchemaProxyHandler.isObject(thenSchema)) {
|
||||||
const result = JsonSchemaProxyHandler.validate(value, thenSchema);
|
JsonSchemaProxyHandler.validate(value, thenSchema);
|
||||||
if (result !== null) { return `then conditional didn't match: ${result}`; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const elseSchema = schema.else;
|
const elseSchema = schema.else;
|
||||||
if (JsonSchemaProxyHandler.isObject(elseSchema)) {
|
if (JsonSchemaProxyHandler.isObject(elseSchema)) {
|
||||||
const result = JsonSchemaProxyHandler.validate(value, thenSchema);
|
JsonSchemaProxyHandler.validate(value, thenSchema);
|
||||||
if (result !== null) { return `else conditional didn't match: ${result}`; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateAllOf(value, schema) {
|
static validateAllOf(value, schema) {
|
||||||
const subSchemas = schema.allOf;
|
const subSchemas = schema.allOf;
|
||||||
if (!Array.isArray(subSchemas)) { return null; }
|
if (!Array.isArray(subSchemas)) { return; }
|
||||||
|
|
||||||
for (let i = 0; i < subSchemas.length; ++i) {
|
for (let i = 0; i < subSchemas.length; ++i) {
|
||||||
const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
||||||
if (result !== null) { return `allOf[${i}] schema didn't match: ${result}`; }
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateAnyOf(value, schema) {
|
static validateAnyOf(value, schema) {
|
||||||
const subSchemas = schema.anyOf;
|
const subSchemas = schema.anyOf;
|
||||||
if (!Array.isArray(subSchemas)) { return null; }
|
if (!Array.isArray(subSchemas)) { return; }
|
||||||
|
|
||||||
for (let i = 0; i < subSchemas.length; ++i) {
|
for (let i = 0; i < subSchemas.length; ++i) {
|
||||||
const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
try {
|
||||||
if (result === null) { return null; }
|
JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
// NOP
|
||||||
}
|
}
|
||||||
return '0 anyOf schemas matched';
|
}
|
||||||
|
|
||||||
|
throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateOneOf(value, schema) {
|
static validateOneOf(value, schema) {
|
||||||
const subSchemas = schema.oneOf;
|
const subSchemas = schema.oneOf;
|
||||||
if (!Array.isArray(subSchemas)) { return null; }
|
if (!Array.isArray(subSchemas)) { return; }
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (let i = 0; i < subSchemas.length; ++i) {
|
for (let i = 0; i < subSchemas.length; ++i) {
|
||||||
const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
try {
|
||||||
if (result === null) { ++count; }
|
JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
||||||
|
++count;
|
||||||
|
} catch (e) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count !== 1) {
|
||||||
|
throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema);
|
||||||
}
|
}
|
||||||
return count === 1 ? null : `${count} oneOf schemas matched`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateNoneOf(value, schema) {
|
static validateNoneOf(value, schema) {
|
||||||
const subSchemas = schema.not;
|
const subSchemas = schema.not;
|
||||||
if (!Array.isArray(subSchemas)) { return null; }
|
if (!Array.isArray(subSchemas)) { return; }
|
||||||
|
|
||||||
for (let i = 0; i < subSchemas.length; ++i) {
|
for (let i = 0; i < subSchemas.length; ++i) {
|
||||||
const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
try {
|
||||||
if (result === null) { return `not[${i}] schema matched`; }
|
JsonSchemaProxyHandler.validate(value, subSchemas[i]);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateSingleSchema(value, schema) {
|
static validateSingleSchema(value, schema) {
|
||||||
const type = JsonSchemaProxyHandler.getValueType(value);
|
const type = JsonSchemaProxyHandler.getValueType(value);
|
||||||
const schemaType = schema.type;
|
const schemaType = schema.type;
|
||||||
if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
|
if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
|
||||||
return `Value type ${type} does not match schema type ${schemaType}`;
|
throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaEnum = schema.enum;
|
const schemaEnum = schema.enum;
|
||||||
if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) {
|
if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) {
|
||||||
return 'Invalid enum value';
|
throw new JsonSchemaValidationError('Invalid enum value', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'number':
|
case 'number':
|
||||||
return JsonSchemaProxyHandler.validateNumber(value, schema);
|
JsonSchemaProxyHandler.validateNumber(value, schema);
|
||||||
|
break;
|
||||||
case 'string':
|
case 'string':
|
||||||
return JsonSchemaProxyHandler.validateString(value, schema);
|
JsonSchemaProxyHandler.validateString(value, schema);
|
||||||
|
break;
|
||||||
case 'array':
|
case 'array':
|
||||||
return JsonSchemaProxyHandler.validateArray(value, schema);
|
JsonSchemaProxyHandler.validateArray(value, schema);
|
||||||
|
break;
|
||||||
case 'object':
|
case 'object':
|
||||||
return JsonSchemaProxyHandler.validateObject(value, schema);
|
JsonSchemaProxyHandler.validateObject(value, schema);
|
||||||
default:
|
break;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateNumber(value, schema) {
|
static validateNumber(value, schema) {
|
||||||
const multipleOf = schema.multipleOf;
|
const multipleOf = schema.multipleOf;
|
||||||
if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
|
if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
|
||||||
return `Number is not a multiple of ${multipleOf}`;
|
throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const minimum = schema.minimum;
|
const minimum = schema.minimum;
|
||||||
if (typeof minimum === 'number' && value < minimum) {
|
if (typeof minimum === 'number' && value < minimum) {
|
||||||
return `Number is less than ${minimum}`;
|
throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exclusiveMinimum = schema.exclusiveMinimum;
|
const exclusiveMinimum = schema.exclusiveMinimum;
|
||||||
if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
|
if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
|
||||||
return `Number is less than or equal to ${exclusiveMinimum}`;
|
throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maximum = schema.maximum;
|
const maximum = schema.maximum;
|
||||||
if (typeof maximum === 'number' && value > maximum) {
|
if (typeof maximum === 'number' && value > maximum) {
|
||||||
return `Number is greater than ${maximum}`;
|
throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exclusiveMaximum = schema.exclusiveMaximum;
|
const exclusiveMaximum = schema.exclusiveMaximum;
|
||||||
if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
|
if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
|
||||||
return `Number is greater than or equal to ${exclusiveMaximum}`;
|
throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateString(value, schema) {
|
static validateString(value, schema) {
|
||||||
const minLength = schema.minLength;
|
const minLength = schema.minLength;
|
||||||
if (typeof minLength === 'number' && value.length < minLength) {
|
if (typeof minLength === 'number' && value.length < minLength) {
|
||||||
return 'String length too short';
|
throw new JsonSchemaValidationError('String length too short', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxLength = schema.maxLength;
|
const maxLength = schema.maxLength;
|
||||||
if (typeof maxLength === 'number' && value.length > maxLength) {
|
if (typeof maxLength === 'number' && value.length > maxLength) {
|
||||||
return 'String length too long';
|
throw new JsonSchemaValidationError('String length too long', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateArray(value, schema) {
|
static validateArray(value, schema) {
|
||||||
const minItems = schema.minItems;
|
const minItems = schema.minItems;
|
||||||
if (typeof minItems === 'number' && value.length < minItems) {
|
if (typeof minItems === 'number' && value.length < minItems) {
|
||||||
return 'Array length too short';
|
throw new JsonSchemaValidationError('Array length too short', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxItems = schema.maxItems;
|
const maxItems = schema.maxItems;
|
||||||
if (typeof maxItems === 'number' && value.length > maxItems) {
|
if (typeof maxItems === 'number' && value.length > maxItems) {
|
||||||
return 'Array length too long';
|
throw new JsonSchemaValidationError('Array length too long', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0, ii = value.length; i < ii; ++i) {
|
for (let i = 0, ii = value.length; i < ii; ++i) {
|
||||||
const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value);
|
const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value);
|
||||||
if (propertySchema === null) {
|
if (propertySchema === null) {
|
||||||
return `No schema found for array[${i}]`;
|
throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = JsonSchemaProxyHandler.validate(value[i], propertySchema);
|
JsonSchemaProxyHandler.validate(value[i], propertySchema);
|
||||||
if (error !== null) {
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static validateObject(value, schema) {
|
static validateObject(value, schema) {
|
||||||
const properties = new Set(Object.getOwnPropertyNames(value));
|
const properties = new Set(Object.getOwnPropertyNames(value));
|
||||||
|
|
||||||
@ -380,35 +366,30 @@ class JsonSchemaProxyHandler {
|
|||||||
if (Array.isArray(required)) {
|
if (Array.isArray(required)) {
|
||||||
for (const property of required) {
|
for (const property of required) {
|
||||||
if (!properties.has(property)) {
|
if (!properties.has(property)) {
|
||||||
return `Missing property ${property}`;
|
throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const minProperties = schema.minProperties;
|
const minProperties = schema.minProperties;
|
||||||
if (typeof minProperties === 'number' && properties.length < minProperties) {
|
if (typeof minProperties === 'number' && properties.length < minProperties) {
|
||||||
return 'Not enough object properties';
|
throw new JsonSchemaValidationError('Not enough object properties', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxProperties = schema.maxProperties;
|
const maxProperties = schema.maxProperties;
|
||||||
if (typeof maxProperties === 'number' && properties.length > maxProperties) {
|
if (typeof maxProperties === 'number' && properties.length > maxProperties) {
|
||||||
return 'Too many object properties';
|
throw new JsonSchemaValidationError('Too many object properties', value, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const property of properties) {
|
for (const property of properties) {
|
||||||
const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value);
|
const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value);
|
||||||
if (propertySchema === null) {
|
if (propertySchema === null) {
|
||||||
return `No schema found for ${property}`;
|
throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema);
|
||||||
}
|
}
|
||||||
const error = JsonSchemaProxyHandler.validate(value[property], propertySchema);
|
JsonSchemaProxyHandler.validate(value[property], propertySchema);
|
||||||
if (error !== null) {
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static isValueTypeAny(value, type, schemaTypes) {
|
static isValueTypeAny(value, type, schemaTypes) {
|
||||||
if (typeof schemaTypes === 'string') {
|
if (typeof schemaTypes === 'string') {
|
||||||
return JsonSchemaProxyHandler.isValueType(value, type, schemaTypes);
|
return JsonSchemaProxyHandler.isValueType(value, type, schemaTypes);
|
||||||
@ -547,6 +528,15 @@ class JsonSchemaProxyHandler {
|
|||||||
|
|
||||||
JsonSchemaProxyHandler._unconstrainedSchema = {};
|
JsonSchemaProxyHandler._unconstrainedSchema = {};
|
||||||
|
|
||||||
|
class JsonSchemaValidationError extends Error {
|
||||||
|
constructor(message, value, schema, path) {
|
||||||
|
super(message);
|
||||||
|
this.value = value;
|
||||||
|
this.schema = schema;
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class JsonSchema {
|
class JsonSchema {
|
||||||
static createProxy(target, schema) {
|
static createProxy(target, schema) {
|
||||||
return new Proxy(target, new JsonSchemaProxyHandler(schema));
|
return new Proxy(target, new JsonSchemaProxyHandler(schema));
|
||||||
|
Loading…
Reference in New Issue
Block a user