json-editor
Version:
JSON Schema based editor
576 lines (531 loc) • 17.6 kB
JavaScript
JSONEditor.Validator = Class.extend({
init: function(jsoneditor,schema,options) {
this.jsoneditor = jsoneditor;
this.schema = schema || this.jsoneditor.schema;
this.options = options || {};
this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
},
validate: function(value) {
return this._validateSchema(this.schema, value);
},
_validateSchema: function(schema,value,path) {
var self = this;
var errors = [];
var valid, i, j;
var stringified = JSON.stringify(value);
path = path || 'root';
// Work on a copy of the schema
schema = $extend({},this.jsoneditor.expandRefs(schema));
/*
* Type Agnostic Validation
*/
// Version 3 `required`
if(schema.required && schema.required === true) {
if(typeof value === "undefined") {
errors.push({
path: path,
property: 'required',
message: this.translate("error_notset")
});
// Can't do any more validation at this point
return errors;
}
}
// Value not defined
else if(typeof value === "undefined") {
// If required_by_default is set, all fields are required
if(this.jsoneditor.options.required_by_default) {
errors.push({
path: path,
property: 'required',
message: this.translate("error_notset")
});
}
// Not required, no further validation needed
else {
return errors;
}
}
// `enum`
if(schema["enum"]) {
valid = false;
for(i=0; i<schema["enum"].length; i++) {
if(stringified === JSON.stringify(schema["enum"][i])) valid = true;
}
if(!valid) {
errors.push({
path: path,
property: 'enum',
message: this.translate("error_enum")
});
}
}
// `extends` (version 3)
if(schema["extends"]) {
for(i=0; i<schema["extends"].length; i++) {
errors = errors.concat(this._validateSchema(schema["extends"][i],value,path));
}
}
// `allOf`
if(schema.allOf) {
for(i=0; i<schema.allOf.length; i++) {
errors = errors.concat(this._validateSchema(schema.allOf[i],value,path));
}
}
// `anyOf`
if(schema.anyOf) {
valid = false;
for(i=0; i<schema.anyOf.length; i++) {
if(!this._validateSchema(schema.anyOf[i],value,path).length) {
valid = true;
break;
}
}
if(!valid) {
errors.push({
path: path,
property: 'anyOf',
message: this.translate('error_anyOf')
});
}
}
// `oneOf`
if(schema.oneOf) {
valid = 0;
var oneof_errors = [];
for(i=0; i<schema.oneOf.length; i++) {
// Set the error paths to be path.oneOf[i].rest.of.path
var tmp = this._validateSchema(schema.oneOf[i],value,path);
if(!tmp.length) {
valid++;
}
for(j=0; j<tmp.length; j++) {
tmp[j].path = path+'.oneOf['+i+']'+tmp[j].path.substr(path.length);
}
oneof_errors = oneof_errors.concat(tmp);
}
if(valid !== 1) {
errors.push({
path: path,
property: 'oneOf',
message: this.translate('error_oneOf', [valid])
});
errors = errors.concat(oneof_errors);
}
}
// `not`
if(schema.not) {
if(!this._validateSchema(schema.not,value,path).length) {
errors.push({
path: path,
property: 'not',
message: this.translate('error_not')
});
}
}
// `type` (both Version 3 and Version 4 support)
if(schema.type) {
// Union type
if(Array.isArray(schema.type)) {
valid = false;
for(i=0;i<schema.type.length;i++) {
if(this._checkType(schema.type[i], value)) {
valid = true;
break;
}
}
if(!valid) {
errors.push({
path: path,
property: 'type',
message: this.translate('error_type_union')
});
}
}
// Simple type
else {
if(!this._checkType(schema.type, value)) {
errors.push({
path: path,
property: 'type',
message: this.translate('error_type', [schema.type])
});
}
}
}
// `disallow` (version 3)
if(schema.disallow) {
// Union type
if(Array.isArray(schema.disallow)) {
valid = true;
for(i=0;i<schema.disallow.length;i++) {
if(this._checkType(schema.disallow[i], value)) {
valid = false;
break;
}
}
if(!valid) {
errors.push({
path: path,
property: 'disallow',
message: this.translate('error_disallow_union')
});
}
}
// Simple type
else {
if(this._checkType(schema.disallow, value)) {
errors.push({
path: path,
property: 'disallow',
message: this.translate('error_disallow', [schema.disallow])
});
}
}
}
/*
* Type Specific Validation
*/
// Number Specific Validation
if(typeof value === "number") {
// `multipleOf` and `divisibleBy`
if(schema.multipleOf || schema.divisibleBy) {
var divisor = schema.multipleOf || schema.divisibleBy;
// Vanilla JS, prone to floating point rounding errors (e.g. 1.14 / .01 == 113.99999)
valid = (value/divisor === Math.floor(value/divisor));
// Use math.js is available
if(window.math) {
valid = window.math.mod(window.math.bignumber(value), window.math.bignumber(divisor)).equals(0);
}
// Use decimal.js is available
else if(window.Decimal) {
valid = (new window.Decimal(value)).mod(new window.Decimal(divisor)).equals(0);
}
if(!valid) {
errors.push({
path: path,
property: schema.multipleOf? 'multipleOf' : 'divisibleBy',
message: this.translate('error_multipleOf', [divisor])
});
}
}
// `maximum`
if(schema.hasOwnProperty('maximum')) {
// Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
valid = schema.exclusiveMaximum? (value < schema.maximum) : (value <= schema.maximum);
// Use math.js is available
if(window.math) {
valid = window.math[schema.exclusiveMaximum?'smaller':'smallerEq'](
window.math.bignumber(value),
window.math.bignumber(schema.maximum)
);
}
// Use Decimal.js if available
else if(window.Decimal) {
valid = (new window.Decimal(value))[schema.exclusiveMaximum?'lt':'lte'](new window.Decimal(schema.maximum));
}
if(!valid) {
errors.push({
path: path,
property: 'maximum',
message: this.translate(
(schema.exclusiveMaximum?'error_maximum_excl':'error_maximum_incl'),
[schema.maximum]
)
});
}
}
// `minimum`
if(schema.hasOwnProperty('minimum')) {
// Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
valid = schema.exclusiveMinimum? (value > schema.minimum) : (value >= schema.minimum);
// Use math.js is available
if(window.math) {
valid = window.math[schema.exclusiveMinimum?'larger':'largerEq'](
window.math.bignumber(value),
window.math.bignumber(schema.minimum)
);
}
// Use Decimal.js if available
else if(window.Decimal) {
valid = (new window.Decimal(value))[schema.exclusiveMinimum?'gt':'gte'](new window.Decimal(schema.minimum));
}
if(!valid) {
errors.push({
path: path,
property: 'minimum',
message: this.translate(
(schema.exclusiveMinimum?'error_minimum_excl':'error_minimum_incl'),
[schema.minimum]
)
});
}
}
}
// String specific validation
else if(typeof value === "string") {
// `maxLength`
if(schema.maxLength) {
if((value+"").length > schema.maxLength) {
errors.push({
path: path,
property: 'maxLength',
message: this.translate('error_maxLength', [schema.maxLength])
});
}
}
// `minLength`
if(schema.minLength) {
if((value+"").length < schema.minLength) {
errors.push({
path: path,
property: 'minLength',
message: this.translate((schema.minLength===1?'error_notempty':'error_minLength'), [schema.minLength])
});
}
}
// `pattern`
if(schema.pattern) {
if(!(new RegExp(schema.pattern)).test(value)) {
errors.push({
path: path,
property: 'pattern',
message: this.translate('error_pattern', [schema.pattern])
});
}
}
}
// Array specific validation
else if(typeof value === "object" && value !== null && Array.isArray(value)) {
// `items` and `additionalItems`
if(schema.items) {
// `items` is an array
if(Array.isArray(schema.items)) {
for(i=0; i<value.length; i++) {
// If this item has a specific schema tied to it
// Validate against it
if(schema.items[i]) {
errors = errors.concat(this._validateSchema(schema.items[i],value[i],path+'.'+i));
}
// If all additional items are allowed
else if(schema.additionalItems === true) {
break;
}
// If additional items is a schema
// TODO: Incompatibility between version 3 and 4 of the spec
else if(schema.additionalItems) {
errors = errors.concat(this._validateSchema(schema.additionalItems,value[i],path+'.'+i));
}
// If no additional items are allowed
else if(schema.additionalItems === false) {
errors.push({
path: path,
property: 'additionalItems',
message: this.translate('error_additionalItems')
});
break;
}
// Default for `additionalItems` is an empty schema
else {
break;
}
}
}
// `items` is a schema
else {
// Each item in the array must validate against the schema
for(i=0; i<value.length; i++) {
errors = errors.concat(this._validateSchema(schema.items,value[i],path+'.'+i));
}
}
}
// `maxItems`
if(schema.maxItems) {
if(value.length > schema.maxItems) {
errors.push({
path: path,
property: 'maxItems',
message: this.translate('error_maxItems', [schema.maxItems])
});
}
}
// `minItems`
if(schema.minItems) {
if(value.length < schema.minItems) {
errors.push({
path: path,
property: 'minItems',
message: this.translate('error_minItems', [schema.minItems])
});
}
}
// `uniqueItems`
if(schema.uniqueItems) {
var seen = {};
for(i=0; i<value.length; i++) {
valid = JSON.stringify(value[i]);
if(seen[valid]) {
errors.push({
path: path,
property: 'uniqueItems',
message: this.translate('error_uniqueItems')
});
break;
}
seen[valid] = true;
}
}
}
// Object specific validation
else if(typeof value === "object" && value !== null) {
// `maxProperties`
if(schema.maxProperties) {
valid = 0;
for(i in value) {
if(!value.hasOwnProperty(i)) continue;
valid++;
}
if(valid > schema.maxProperties) {
errors.push({
path: path,
property: 'maxProperties',
message: this.translate('error_maxProperties', [schema.maxProperties])
});
}
}
// `minProperties`
if(schema.minProperties) {
valid = 0;
for(i in value) {
if(!value.hasOwnProperty(i)) continue;
valid++;
}
if(valid < schema.minProperties) {
errors.push({
path: path,
property: 'minProperties',
message: this.translate('error_minProperties', [schema.minProperties])
});
}
}
// Version 4 `required`
if(schema.required && Array.isArray(schema.required)) {
for(i=0; i<schema.required.length; i++) {
if(typeof value[schema.required[i]] === "undefined") {
errors.push({
path: path,
property: 'required',
message: this.translate('error_required', [schema.required[i]])
});
}
}
}
// `properties`
var validated_properties = {};
if(schema.properties) {
for(i in schema.properties) {
if(!schema.properties.hasOwnProperty(i)) continue;
validated_properties[i] = true;
errors = errors.concat(this._validateSchema(schema.properties[i],value[i],path+'.'+i));
}
}
// `patternProperties`
if(schema.patternProperties) {
for(i in schema.patternProperties) {
if(!schema.patternProperties.hasOwnProperty(i)) continue;
var regex = new RegExp(i);
// Check which properties match
for(j in value) {
if(!value.hasOwnProperty(j)) continue;
if(regex.test(j)) {
validated_properties[j] = true;
errors = errors.concat(this._validateSchema(schema.patternProperties[i],value[j],path+'.'+j));
}
}
}
}
// The no_additional_properties option currently doesn't work with extended schemas that use oneOf or anyOf
if(typeof schema.additionalProperties === "undefined" && this.jsoneditor.options.no_additional_properties && !schema.oneOf && !schema.anyOf) {
schema.additionalProperties = false;
}
// `additionalProperties`
if(typeof schema.additionalProperties !== "undefined") {
for(i in value) {
if(!value.hasOwnProperty(i)) continue;
if(!validated_properties[i]) {
// No extra properties allowed
if(!schema.additionalProperties) {
errors.push({
path: path,
property: 'additionalProperties',
message: this.translate('error_additional_properties', [i])
});
break;
}
// Allowed
else if(schema.additionalProperties === true) {
break;
}
// Must match schema
// TODO: incompatibility between version 3 and 4 of the spec
else {
errors = errors.concat(this._validateSchema(schema.additionalProperties,value[i],path+'.'+i));
}
}
}
}
// `dependencies`
if(schema.dependencies) {
for(i in schema.dependencies) {
if(!schema.dependencies.hasOwnProperty(i)) continue;
// Doesn't need to meet the dependency
if(typeof value[i] === "undefined") continue;
// Property dependency
if(Array.isArray(schema.dependencies[i])) {
for(j=0; j<schema.dependencies[i].length; j++) {
if(typeof value[schema.dependencies[i][j]] === "undefined") {
errors.push({
path: path,
property: 'dependencies',
message: this.translate('error_dependency', [schema.dependencies[i][j]])
});
}
}
}
// Schema dependency
else {
errors = errors.concat(this._validateSchema(schema.dependencies[i],value,path));
}
}
}
}
// Custom type validation (global)
$each(JSONEditor.defaults.custom_validators,function(i,validator) {
errors = errors.concat(validator.call(self,schema,value,path));
});
// Custom type validation (instance specific)
if(this.options.custom_validators) {
$each(this.options.custom_validators,function(i,validator) {
errors = errors.concat(validator.call(self,schema,value,path));
});
}
return errors;
},
_checkType: function(type, value) {
// Simple types
if(typeof type === "string") {
if(type==="string") return typeof value === "string";
else if(type==="number") return typeof value === "number";
else if(type==="integer") return typeof value === "number" && value === Math.floor(value);
else if(type==="boolean") return typeof value === "boolean";
else if(type==="array") return Array.isArray(value);
else if(type === "object") return value !== null && !(Array.isArray(value)) && typeof value === "object";
else if(type === "null") return value === null;
else return true;
}
// Schema
else {
return !this._validateSchema(type,value).length;
}
}
});