// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //============================================================================== // This file contains a class that implements a subset of JSON Schema. // See: http://www.json.com/json-schema-proposal/ for more details. // // The following features of JSON Schema are not implemented: // - requires // - unique // - disallow // - union types // // The following properties are not applicable to the interface exposed by // this class: // - options // - readonly // - title // - description // - format // - default // - transient // - hidden //============================================================================== var chromium = chromium || {}; /** * Validates an instance against a schema and accumulates errors. Usage: * * var validator = new chromium.JSONSchemaValidator(); * validator.validate(inst, schema); * if (validator.errors.length == 0) * console.log("Valid!"); * else * console.log(validator.errors); * * The errors property contains a list of objects. Each object has two * properties: "path" and "message". The "path" property contains the path to * the key that had the problem, and the "message" property contains a sentence * describing the error. */ chromium.JSONSchemaValidator = function() { this.errors = []; }; chromium.JSONSchemaValidator.messages = { invalidEnum: "Value must be one of: [*].", propertyRequired: "Property is required.", unexpectedProperty: "Unexpected property.", arrayMinItems: "Array must have at least * items.", arrayMaxItems: "Array must not have more than * items.", itemRequired: "Item is required.", stringMinLength: "String must be at least * characters long.", stringMaxLength: "String must not be more than * characters long.", stringPattern: "String must match the pattern: *.", numberMinValue: "Value must not be less than *.", numberMaxValue: "Value must not be greater than *.", numberMaxDecimal: "Value must not have more than * decimal places.", invalidType: "Expected '*' but got '*'." }; /** * Builds an error message. Key is the property in the |errors| object, and * |opt_replacements| is an array of values to replace "*" characters with. */ chromium.JSONSchemaValidator.formatError = function(key, opt_replacements) { var message = this.messages[key]; if (opt_replacements) { for (var i = 0; i < opt_replacements.length; i++) { message = message.replace("*", opt_replacements[i]); } } return message; }; /** * Classifies a value as one of the JSON schema primitive types. Note that we * don't explicitly disallow 'function', because we want to allow functions in * the input values. */ chromium.JSONSchemaValidator.getType = function(value) { var s = typeof value; if (s == "object") { if (value === null) { return "null"; } else if (value instanceof Array || Object.prototype.toString.call(value) == "[Object Array]") { return "array"; } } else if (s == "number") { if (value % 1 == 0) { return "integer"; } } return s; }; /** * Validates an instance against a schema. The instance can be any JavaScript * value and will be validated recursively. When this method returns, the * |errors| property will contain a list of errors, if any. */ chromium.JSONSchemaValidator.prototype.validate = function(instance, schema, opt_path) { var path = opt_path || ""; // If the schema has an extends property, the instance must validate against // that schema too. if (schema.extends) this.validate(instance, schema.extends, path); // If the schema has an enum property, the instance must be one of those // values. if (schema.enum) { if (!this.validateEnum(instance, schema, path)) return; } if (schema.type && schema.type != "any") { if (!this.validateType(instance, schema, path)) return; // Type-specific validation. switch (schema.type) { case "object": this.validateObject(instance, schema, path); break; case "array": this.validateArray(instance, schema, path); break; case "string": this.validateString(instance, schema, path); break; case "number": case "integer": this.validateNumber(instance, schema, path); break; } } }; /** * Validates an instance against a schema with an enum type. Populates the * |errors| property, and returns a boolean indicating whether the instance * validates. */ chromium.JSONSchemaValidator.prototype.validateEnum = function(instance, schema, path) { for (var i = 0; i < schema.enum.length; i++) { if (instance === schema.enum[i]) return true; } this.addError(path, "invalidEnum", [schema.enum.join(", ")]); return false; }; /** * Validates an instance against an object schema and populates the errors * property. */ chromium.JSONSchemaValidator.prototype.validateObject = function(instance, schema, path) { for (var prop in schema.properties) { var propPath = path ? path + "." + prop : prop; if (instance.hasOwnProperty(prop)) { this.validate(instance[prop], schema.properties[prop], propPath); } else if (!schema.properties[prop].optional) { this.addError(propPath, "propertyRequired"); } } // The additionalProperties property can either be |false| or a schema // definition. If |false|, additional properties are not allowed. If a schema // defintion, all additional properties must validate against that schema. if (typeof schema.additionalProperties != "undefined") { for (var prop in instance) { if (instance.hasOwnProperty(prop)) { var propPath = path ? path + "." + prop : prop; if (!schema.properties.hasOwnProperty(prop)) { if (schema.additionalProperties === false) this.addError(propPath, "unexpectedProperty"); else this.validate(instance[prop], schema.additionalProperties, propPath); } } } } }; /** * Validates an instance against an array schema and populates the errors * property. */ chromium.JSONSchemaValidator.prototype.validateArray = function(instance, schema, path) { var typeOfItems = chromium.JSONSchemaValidator.getType(schema.items); if (typeOfItems == 'object') { if (schema.minItems && instance.length < schema.minItems) { this.addError(path, "arrayMinItems", [schema.minItems]); } if (typeof schema.maxItems != "undefined" && instance.length > schema.maxItems) { this.addError(path, "arrayMaxItems", [schema.maxItems]); } // If the items property is a single schema, each item in the array must // have that schema. for (var i = 0; i < instance.length; i++) { this.validate(instance[i], schema.items, path + "[" + i + "]"); } } else if (typeOfItems == 'array') { // If the items property is an array of schemas, each item in the array must // validate against the corresponding schema. for (var i = 0; i < schema.items.length; i++) { var itemPath = path ? path + "[" + i + "]" : String(i); if (instance.hasOwnProperty(i)) { this.validate(instance[i], schema.items[i], itemPath); } else if (!schema.items[i].optional) { this.addError(itemPath, "itemRequired"); } } if (schema.additionalProperties === false) { if (instance.length > schema.items.length) { this.addError(path, "arrayMaxItems", [schema.items.length]); } } else if (schema.additionalProperties) { for (var i = schema.items.length; i < instance.length; i++) { var itemPath = path ? path + "[" + i + "]" : String(i); this.validate(instance[i], schema.additionalProperties, itemPath); } } } }; /** * Validates a string and populates the errors property. */ chromium.JSONSchemaValidator.prototype.validateString = function(instance, schema, path) { if (schema.minLength && instance.length < schema.minLength) this.addError(path, "stringMinLength", [schema.minLength]); if (schema.maxLength && instance.length > schema.maxLength) this.addError(path, "stringMaxLength", [schema.maxLength]); if (schema.pattern && !schema.pattern.test(instance)) this.addError(path, "stringPattern", [schema.pattern]); }; /** * Validates a number and populates the errors property. The instance is * assumed to be a number. */ chromium.JSONSchemaValidator.prototype.validateNumber = function(instance, schema, path) { if (schema.minimum && instance < schema.minimum) this.addError(path, "numberMinValue", [schema.minimum]); if (schema.maximum && instance > schema.maximum) this.addError(path, "numberMaxValue", [schema.maximum]); if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); }; /** * Validates the primitive type of an instance and populates the errors * property. Returns true if the instance validates, false otherwise. */ chromium.JSONSchemaValidator.prototype.validateType = function(instance, schema, path) { var actualType = chromium.JSONSchemaValidator.getType(instance); if (schema.type != actualType && !(schema.type == "number" && actualType == "integer")) { this.addError(path, "invalidType", [schema.type, actualType]); return false; } return true; }; /** * Adds an error message. |key| is an index into the |messages| object. * |replacements| is an array of values to replace '*' characters in the * message. */ chromium.JSONSchemaValidator.prototype.addError = function(path, key, replacements) { this.errors.push({ path: path, message: chromium.JSONSchemaValidator.formatError(key, replacements) }); }; // Set up chromium.types with some commonly used types... (function() { function extend(base, ext) { var result = {}; for (var p in base) result[p] = base[p]; for (var p in ext) result[p] = ext[p]; return result; } var types = {}; types.opt = {optional: true}; types.bool = {type: "boolean"}; types.int = {type: "integer"}; types.str = {type: "string"}; types.fun = {type: "function"}; types.pInt = extend(types.int, {minimum: 0}); types.optBool = extend(types.bool, types.opt); types.optInt = extend(types.int, types.opt); types.optStr = extend(types.str, types.opt); types.optFun = extend(types.fun, types.opt); types.optPInt = extend(types.pInt, types.opt); chromium.types = types; })();