1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // -----------------------------------------------------------------------------
6 // NOTE: If you change this file you need to touch
7 // extension_renderer_resources.grd to have your change take effect.
8 // -----------------------------------------------------------------------------
10 //==============================================================================
11 // This file contains a class that implements a subset of JSON Schema.
12 // See: http://www.json.com/json-schema-proposal/ for more details.
14 // The following features of JSON Schema are not implemented:
18 // - union types (but replaced with 'choices')
20 // The following properties are not applicable to the interface exposed by
31 // There are also these departures from the JSON Schema proposal:
32 // - function and undefined types are supported
33 // - null counts as 'unspecified' for optional values
34 // - added the 'choices' property, to allow specifying a list of possible types
36 // - by default an "object" typed schema does not allow additional properties.
37 // if present, "additionalProperties" is to be a schema against which all
38 // additional properties will be validated.
39 //==============================================================================
41 var loadTypeSchema
= require('utils').loadTypeSchema
;
42 var CHECK
= requireNative('logging').CHECK
;
44 function isInstanceOfClass(instance
, className
) {
45 while ((instance
= instance
.__proto__
)) {
46 if (instance
.constructor.name
== className
)
52 function isOptionalValue(value
) {
53 return typeof(value
) === 'undefined' || value
=== null;
56 function enumToString(enumValue
) {
57 if (enumValue
.name
=== undefined)
60 return enumValue
.name
;
64 * Validates an instance against a schema and accumulates errors. Usage:
66 * var validator = new JSONSchemaValidator();
67 * validator.validate(inst, schema);
68 * if (validator.errors.length == 0)
69 * console.log("Valid!");
71 * console.log(validator.errors);
73 * The errors property contains a list of objects. Each object has two
74 * properties: "path" and "message". The "path" property contains the path to
75 * the key that had the problem, and the "message" property contains a sentence
76 * describing the error.
78 function JSONSchemaValidator() {
83 JSONSchemaValidator
.messages
= {
84 invalidEnum
: "Value must be one of: [*].",
85 propertyRequired
: "Property is required.",
86 unexpectedProperty
: "Unexpected property.",
87 arrayMinItems
: "Array must have at least * items.",
88 arrayMaxItems
: "Array must not have more than * items.",
89 itemRequired
: "Item is required.",
90 stringMinLength
: "String must be at least * characters long.",
91 stringMaxLength
: "String must not be more than * characters long.",
92 stringPattern
: "String must match the pattern: *.",
93 numberFiniteNotNan
: "Value must not be *.",
94 numberMinValue
: "Value must not be less than *.",
95 numberMaxValue
: "Value must not be greater than *.",
96 numberIntValue
: "Value must fit in a 32-bit signed integer.",
97 numberMaxDecimal
: "Value must not have more than * decimal places.",
98 invalidType
: "Expected '*' but got '*'.",
99 invalidTypeIntegerNumber
:
100 "Expected 'integer' but got 'number', consider using Math.round().",
101 invalidChoice
: "Value does not match any valid type choices.",
102 invalidPropertyType
: "Missing property type.",
103 schemaRequired
: "Schema value required.",
104 unknownSchemaReference
: "Unknown schema reference: *.",
105 notInstance
: "Object must be an instance of *."
109 * Builds an error message. Key is the property in the |errors| object, and
110 * |opt_replacements| is an array of values to replace "*" characters with.
112 JSONSchemaValidator
.formatError = function(key
, opt_replacements
) {
113 var message
= this.messages
[key
];
114 if (opt_replacements
) {
115 for (var i
= 0; i
< opt_replacements
.length
; i
++) {
116 message
= message
.replace("*", opt_replacements
[i
]);
123 * Classifies a value as one of the JSON schema primitive types. Note that we
124 * don't explicitly disallow 'function', because we want to allow functions in
127 JSONSchemaValidator
.getType = function(value
) {
128 var s
= typeof value
;
131 if (value
=== null) {
133 } else if (Object
.prototype.toString
.call(value
) == "[object Array]") {
135 } else if (typeof(ArrayBuffer
) != "undefined" &&
136 value
.constructor == ArrayBuffer
) {
139 } else if (s
== "number") {
140 if (value
% 1 == 0) {
149 * Add types that may be referenced by validated schemas that reference them
150 * with "$ref": <typeId>. Each type must be a valid schema and define an
153 JSONSchemaValidator
.prototype.addTypes = function(typeOrTypeList
) {
154 function addType(validator
, type
) {
156 throw new Error("Attempt to addType with missing 'id' property");
157 validator
.types
[type
.id
] = type
;
160 if (typeOrTypeList
instanceof Array
) {
161 for (var i
= 0; i
< typeOrTypeList
.length
; i
++) {
162 addType(this, typeOrTypeList
[i
]);
165 addType(this, typeOrTypeList
);
170 * Returns a list of strings of the types that this schema accepts.
172 JSONSchemaValidator
.prototype.getAllTypesForSchema = function(schema
) {
173 var schemaTypes
= [];
175 $Array
.push(schemaTypes
, schema
.type
);
176 if (schema
.choices
) {
177 for (var i
= 0; i
< schema
.choices
.length
; i
++) {
178 var choiceTypes
= this.getAllTypesForSchema(schema
.choices
[i
]);
179 schemaTypes
= $Array
.concat(schemaTypes
, choiceTypes
);
182 var ref
= schema
['$ref'];
184 var type
= this.getOrAddType(ref
);
185 CHECK(type
, 'Could not find type ' + ref
);
186 schemaTypes
= $Array
.concat(schemaTypes
, this.getAllTypesForSchema(type
));
191 JSONSchemaValidator
.prototype.getOrAddType = function(typeName
) {
192 if (!this.types
[typeName
])
193 this.types
[typeName
] = loadTypeSchema(typeName
);
194 return this.types
[typeName
];
198 * Returns true if |schema| would accept an argument of type |type|.
200 JSONSchemaValidator
.prototype.isValidSchemaType = function(type
, schema
) {
204 // TODO(kalman): I don't understand this code. How can type be "null"?
205 if (schema
.optional
&& (type
== "null" || type
== "undefined"))
208 var schemaTypes
= this.getAllTypesForSchema(schema
);
209 for (var i
= 0; i
< schemaTypes
.length
; i
++) {
210 if (schemaTypes
[i
] == "any" || type
== schemaTypes
[i
] ||
211 (type
== "integer" && schemaTypes
[i
] == "number"))
219 * Returns true if there is a non-null argument that both |schema1| and
220 * |schema2| would accept.
222 JSONSchemaValidator
.prototype.checkSchemaOverlap = function(schema1
, schema2
) {
223 var schema1Types
= this.getAllTypesForSchema(schema1
);
224 for (var i
= 0; i
< schema1Types
.length
; i
++) {
225 if (this.isValidSchemaType(schema1Types
[i
], schema2
))
232 * Validates an instance against a schema. The instance can be any JavaScript
233 * value and will be validated recursively. When this method returns, the
234 * |errors| property will contain a list of errors, if any.
236 JSONSchemaValidator
.prototype.validate = function(instance
, schema
, opt_path
) {
237 var path
= opt_path
|| "";
240 this.addError(path
, "schemaRequired");
244 // If this schema defines itself as reference type, save it in this.types.
246 this.types
[schema
.id
] = schema
;
248 // If the schema has an extends property, the instance must validate against
251 this.validate(instance
, schema
.extends, path
);
253 // If the schema has a $ref property, the instance must validate against
254 // that schema too. It must be present in this.types to be referenced.
255 var ref
= schema
["$ref"];
257 if (!this.getOrAddType(ref
))
258 this.addError(path
, "unknownSchemaReference", [ ref
]);
260 this.validate(instance
, this.getOrAddType(ref
), path
)
263 // If the schema has a choices property, the instance must validate against at
264 // least one of the items in that array.
265 if (schema
.choices
) {
266 this.validateChoices(instance
, schema
, path
);
270 // If the schema has an enum property, the instance must be one of those
273 if (!this.validateEnum(instance
, schema
, path
))
277 if (schema
.type
&& schema
.type
!= "any") {
278 if (!this.validateType(instance
, schema
, path
))
281 // Type-specific validation.
282 switch (schema
.type
) {
284 this.validateObject(instance
, schema
, path
);
287 this.validateArray(instance
, schema
, path
);
290 this.validateString(instance
, schema
, path
);
294 this.validateNumber(instance
, schema
, path
);
301 * Validates an instance against a choices schema. The instance must match at
302 * least one of the provided choices.
304 JSONSchemaValidator
.prototype.validateChoices
=
305 function(instance
, schema
, path
) {
306 var originalErrors
= this.errors
;
308 for (var i
= 0; i
< schema
.choices
.length
; i
++) {
310 this.validate(instance
, schema
.choices
[i
], path
);
311 if (this.errors
.length
== 0) {
312 this.errors
= originalErrors
;
317 this.errors
= originalErrors
;
318 this.addError(path
, "invalidChoice");
322 * Validates an instance against a schema with an enum type. Populates the
323 * |errors| property, and returns a boolean indicating whether the instance
326 JSONSchemaValidator
.prototype.validateEnum = function(instance
, schema
, path
) {
327 for (var i
= 0; i
< schema
.enum.length
; i
++) {
328 if (instance
=== enumToString(schema
.enum[i
]))
332 this.addError(path
, "invalidEnum",
333 [schema
.enum.map(enumToString
).join(", ")]);
338 * Validates an instance against an object schema and populates the errors
341 JSONSchemaValidator
.prototype.validateObject
=
342 function(instance
, schema
, path
) {
343 if (schema
.properties
) {
344 for (var prop
in schema
.properties
) {
345 // It is common in JavaScript to add properties to Object.prototype. This
346 // check prevents such additions from being interpreted as required
347 // schema properties.
348 // TODO(aa): If it ever turns out that we actually want this to work,
349 // there are other checks we could put here, like requiring that schema
350 // properties be objects that have a 'type' property.
351 if (!$Object
.hasOwnProperty(schema
.properties
, prop
))
354 var propPath
= path
? path
+ "." + prop
: prop
;
355 if (schema
.properties
[prop
] == undefined) {
356 this.addError(propPath
, "invalidPropertyType");
357 } else if (prop
in instance
&& !isOptionalValue(instance
[prop
])) {
358 this.validate(instance
[prop
], schema
.properties
[prop
], propPath
);
359 } else if (!schema
.properties
[prop
].optional
) {
360 this.addError(propPath
, "propertyRequired");
365 // If "instanceof" property is set, check that this object inherits from
366 // the specified constructor (function).
367 if (schema
.isInstanceOf
) {
368 if (!isInstanceOfClass(instance
, schema
.isInstanceOf
))
369 this.addError(propPath
, "notInstance", [schema
.isInstanceOf
]);
372 // Exit early from additional property check if "type":"any" is defined.
373 if (schema
.additionalProperties
&&
374 schema
.additionalProperties
.type
&&
375 schema
.additionalProperties
.type
== "any") {
379 // By default, additional properties are not allowed on instance objects. This
380 // can be overridden by setting the additionalProperties property to a schema
381 // which any additional properties must validate against.
382 for (var prop
in instance
) {
383 if (schema
.properties
&& prop
in schema
.properties
)
386 // Any properties inherited through the prototype are ignored.
387 if (!$Object
.hasOwnProperty(instance
, prop
))
390 var propPath
= path
? path
+ "." + prop
: prop
;
391 if (schema
.additionalProperties
)
392 this.validate(instance
[prop
], schema
.additionalProperties
, propPath
);
394 this.addError(propPath
, "unexpectedProperty");
399 * Validates an instance against an array schema and populates the errors
402 JSONSchemaValidator
.prototype.validateArray = function(instance
, schema
, path
) {
403 var typeOfItems
= JSONSchemaValidator
.getType(schema
.items
);
405 if (typeOfItems
== 'object') {
406 if (schema
.minItems
&& instance
.length
< schema
.minItems
) {
407 this.addError(path
, "arrayMinItems", [schema
.minItems
]);
410 if (typeof schema
.maxItems
!= "undefined" &&
411 instance
.length
> schema
.maxItems
) {
412 this.addError(path
, "arrayMaxItems", [schema
.maxItems
]);
415 // If the items property is a single schema, each item in the array must
417 for (var i
= 0; i
< instance
.length
; i
++) {
418 this.validate(instance
[i
], schema
.items
, path
+ "." + i
);
420 } else if (typeOfItems
== 'array') {
421 // If the items property is an array of schemas, each item in the array must
422 // validate against the corresponding schema.
423 for (var i
= 0; i
< schema
.items
.length
; i
++) {
424 var itemPath
= path
? path
+ "." + i
: String(i
);
425 if (i
in instance
&& !isOptionalValue(instance
[i
])) {
426 this.validate(instance
[i
], schema
.items
[i
], itemPath
);
427 } else if (!schema
.items
[i
].optional
) {
428 this.addError(itemPath
, "itemRequired");
432 if (schema
.additionalProperties
) {
433 for (var i
= schema
.items
.length
; i
< instance
.length
; i
++) {
434 var itemPath
= path
? path
+ "." + i
: String(i
);
435 this.validate(instance
[i
], schema
.additionalProperties
, itemPath
);
438 if (instance
.length
> schema
.items
.length
) {
439 this.addError(path
, "arrayMaxItems", [schema
.items
.length
]);
446 * Validates a string and populates the errors property.
448 JSONSchemaValidator
.prototype.validateString
=
449 function(instance
, schema
, path
) {
450 if (schema
.minLength
&& instance
.length
< schema
.minLength
)
451 this.addError(path
, "stringMinLength", [schema
.minLength
]);
453 if (schema
.maxLength
&& instance
.length
> schema
.maxLength
)
454 this.addError(path
, "stringMaxLength", [schema
.maxLength
]);
456 if (schema
.pattern
&& !schema
.pattern
.test(instance
))
457 this.addError(path
, "stringPattern", [schema
.pattern
]);
461 * Validates a number and populates the errors property. The instance is
462 * assumed to be a number.
464 JSONSchemaValidator
.prototype.validateNumber
=
465 function(instance
, schema
, path
) {
466 // Forbid NaN, +Infinity, and -Infinity. Our APIs don't use them, and
467 // JSON serialization encodes them as 'null'. Re-evaluate supporting
468 // them if we add an API that could reasonably take them as a parameter.
469 if (isNaN(instance
) ||
470 instance
== Number
.POSITIVE_INFINITY
||
471 instance
== Number
.NEGATIVE_INFINITY
)
472 this.addError(path
, "numberFiniteNotNan", [instance
]);
474 if (schema
.minimum
!== undefined && instance
< schema
.minimum
)
475 this.addError(path
, "numberMinValue", [schema
.minimum
]);
477 if (schema
.maximum
!== undefined && instance
> schema
.maximum
)
478 this.addError(path
, "numberMaxValue", [schema
.maximum
]);
480 // Check for integer values outside of -2^31..2^31-1.
481 if (schema
.type
=== "integer" && (instance
| 0) !== instance
)
482 this.addError(path
, "numberIntValue", []);
484 if (schema
.maxDecimal
&& instance
* Math
.pow(10, schema
.maxDecimal
) % 1)
485 this.addError(path
, "numberMaxDecimal", [schema
.maxDecimal
]);
489 * Validates the primitive type of an instance and populates the errors
490 * property. Returns true if the instance validates, false otherwise.
492 JSONSchemaValidator
.prototype.validateType = function(instance
, schema
, path
) {
493 var actualType
= JSONSchemaValidator
.getType(instance
);
494 if (schema
.type
== actualType
||
495 (schema
.type
== "number" && actualType
== "integer")) {
497 } else if (schema
.type
== "integer" && actualType
== "number") {
498 this.addError(path
, "invalidTypeIntegerNumber");
501 this.addError(path
, "invalidType", [schema
.type
, actualType
]);
507 * Adds an error message. |key| is an index into the |messages| object.
508 * |replacements| is an array of values to replace '*' characters in the
511 JSONSchemaValidator
.prototype.addError = function(path
, key
, replacements
) {
512 $Array
.push(this.errors
, {
514 message
: JSONSchemaValidator
.formatError(key
, replacements
)
519 * Resets errors to an empty list so you can call 'validate' again.
521 JSONSchemaValidator
.prototype.resetErrors = function() {
525 exports
.JSONSchemaValidator
= JSONSchemaValidator
;