1
|
|
|
/** |
2
|
|
|
* JSONSchema Validator - Validates JavaScript objects using JSON Schemas |
3
|
|
|
* (http://www.json.com/json-schema-proposal/) |
4
|
|
|
* |
5
|
|
|
* Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com) |
6
|
|
|
* Licensed under the MIT (MIT-LICENSE.txt) license. |
7
|
|
|
To use the validator call the validate function with an instance object and an optional schema object. |
8
|
|
|
If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating), |
9
|
|
|
that schema will be used to validate and the schema parameter is not necessary (if both exist, |
10
|
|
|
both validations will occur). |
11
|
|
|
The validate method will return an array of validation errors. If there are no errors, then an |
12
|
|
|
empty list will be returned. A validation error will have two properties: |
13
|
|
|
"property" which indicates which property had the error |
14
|
|
|
"message" which indicates what the error was |
15
|
|
|
*/ |
16
|
|
|
(function (root, factory) { |
17
|
|
|
if (typeof define === 'function' && define.amd) { |
18
|
|
|
// AMD. Register as an anonymous module. |
19
|
|
|
define([], function () { |
20
|
|
|
return factory(); |
21
|
|
|
}); |
22
|
|
|
} else if (typeof module === 'object' && module.exports) { |
23
|
|
|
// Node. Does not work with strict CommonJS, but |
24
|
|
|
// only CommonJS-like environments that support module.exports, |
25
|
|
|
// like Node. |
26
|
|
|
module.exports = factory(); |
27
|
|
|
} else { |
28
|
|
|
// Browser globals |
29
|
|
|
root.jsonSchema = factory(); |
30
|
|
|
} |
31
|
|
|
}(this, function () {// setup primitive classes to be JSON Schema types |
32
|
|
|
var exports = validate |
33
|
|
|
exports.Integer = {type:"integer"}; |
34
|
|
|
var primitiveConstructors = { |
35
|
|
|
String: String, |
36
|
|
|
Boolean: Boolean, |
37
|
|
|
Number: Number, |
38
|
|
|
Object: Object, |
39
|
|
|
Array: Array, |
40
|
|
|
Date: Date |
41
|
|
|
} |
42
|
|
|
exports.validate = validate; |
43
|
|
|
function validate(/*Any*/instance,/*Object*/schema) { |
44
|
|
|
// Summary: |
45
|
|
|
// To use the validator call JSONSchema.validate with an instance object and an optional schema object. |
46
|
|
|
// If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating), |
47
|
|
|
// that schema will be used to validate and the schema parameter is not necessary (if both exist, |
48
|
|
|
// both validations will occur). |
49
|
|
|
// The validate method will return an object with two properties: |
50
|
|
|
// valid: A boolean indicating if the instance is valid by the schema |
51
|
|
|
// errors: An array of validation errors. If there are no errors, then an |
52
|
|
|
// empty list will be returned. A validation error will have two properties: |
53
|
|
|
// property: which indicates which property had the error |
54
|
|
|
// message: which indicates what the error was |
55
|
|
|
// |
56
|
|
|
return validate(instance, schema, {changing: false});//, coerce: false, existingOnly: false}); |
|
|
|
|
57
|
|
|
}; |
58
|
|
|
exports.checkPropertyChange = function(/*Any*/value,/*Object*/schema, /*String*/property) { |
59
|
|
|
// Summary: |
60
|
|
|
// The checkPropertyChange method will check to see if an value can legally be in property with the given schema |
61
|
|
|
// This is slightly different than the validate method in that it will fail if the schema is readonly and it will |
62
|
|
|
// not check for self-validation, it is assumed that the passed in value is already internally valid. |
63
|
|
|
// The checkPropertyChange method will return the same object type as validate, see JSONSchema.validate for |
64
|
|
|
// information. |
65
|
|
|
// |
66
|
|
|
return validate(value, schema, {changing: property || "property"}); |
|
|
|
|
67
|
|
|
}; |
68
|
|
|
var validate = exports._validate = function(/*Any*/instance,/*Object*/schema,/*Object*/options) { |
|
|
|
|
69
|
|
|
|
70
|
|
|
if (!options) options = {}; |
71
|
|
|
var _changing = options.changing; |
72
|
|
|
|
73
|
|
|
function getType(schema){ |
74
|
|
|
return schema.type || (primitiveConstructors[schema.name] == schema && schema.name.toLowerCase()); |
75
|
|
|
} |
76
|
|
|
var errors = []; |
77
|
|
|
// validate a value against a property definition |
78
|
|
|
function checkProp(value, schema, path,i){ |
79
|
|
|
|
80
|
|
|
var l; |
81
|
|
|
path += path ? typeof i == 'number' ? '[' + i + ']' : typeof i == 'undefined' ? '' : '.' + i : i; |
82
|
|
|
function addError(message){ |
83
|
|
|
errors.push({property:path,message:message}); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
if((typeof schema != 'object' || schema instanceof Array) && (path || typeof schema != 'function') && !(schema && getType(schema))){ |
87
|
|
|
if(typeof schema == 'function'){ |
88
|
|
|
if(!(value instanceof schema)){ |
89
|
|
|
addError("is not an instance of the class/constructor " + schema.name); |
90
|
|
|
} |
91
|
|
|
}else if(schema){ |
92
|
|
|
addError("Invalid schema/property definition " + schema); |
93
|
|
|
} |
94
|
|
|
return null; |
95
|
|
|
} |
96
|
|
|
if(_changing && schema.readonly){ |
97
|
|
|
addError("is a readonly field, it can not be changed"); |
98
|
|
|
} |
99
|
|
|
if(schema['extends']){ // if it extends another schema, it must pass that schema as well |
100
|
|
|
checkProp(value,schema['extends'],path,i); |
101
|
|
|
} |
102
|
|
|
// validate a value against a type definition |
103
|
|
|
function checkType(type,value){ |
104
|
|
|
if(type){ |
105
|
|
|
if(typeof type == 'string' && type != 'any' && |
106
|
|
|
(type == 'null' ? value !== null : typeof value != type) && |
107
|
|
|
!(value instanceof Array && type == 'array') && |
108
|
|
|
!(value instanceof Date && type == 'date') && |
109
|
|
|
!(type == 'integer' && value%1===0)){ |
110
|
|
|
return [{property:path,message:(typeof value) + " value found, but a " + type + " is required"}]; |
111
|
|
|
} |
112
|
|
|
if(type instanceof Array){ |
113
|
|
|
var unionErrors=[]; |
114
|
|
|
for(var j = 0; j < type.length; j++){ // a union type |
115
|
|
|
if(!(unionErrors=checkType(type[j],value)).length){ |
116
|
|
|
break; |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
if(unionErrors.length){ |
120
|
|
|
return unionErrors; |
121
|
|
|
} |
122
|
|
|
}else if(typeof type == 'object'){ |
123
|
|
|
var priorErrors = errors; |
124
|
|
|
errors = []; |
125
|
|
|
checkProp(value,type,path); |
126
|
|
|
var theseErrors = errors; |
127
|
|
|
errors = priorErrors; |
128
|
|
|
return theseErrors; |
129
|
|
|
} |
130
|
|
|
} |
131
|
|
|
return []; |
132
|
|
|
} |
133
|
|
|
if(value === undefined){ |
134
|
|
|
if(schema.required){ |
135
|
|
|
addError("is missing and it is required"); |
136
|
|
|
} |
137
|
|
|
}else{ |
138
|
|
|
errors = errors.concat(checkType(getType(schema),value)); |
139
|
|
|
if(schema.disallow && !checkType(schema.disallow,value).length){ |
140
|
|
|
addError(" disallowed value was matched"); |
141
|
|
|
} |
142
|
|
|
if(value !== null){ |
143
|
|
|
if(value instanceof Array){ |
144
|
|
|
if(schema.items){ |
145
|
|
|
var itemsIsArray = schema.items instanceof Array; |
146
|
|
|
var propDef = schema.items; |
147
|
|
|
for (i = 0, l = value.length; i < l; i += 1) { |
148
|
|
|
if (itemsIsArray) |
149
|
|
|
propDef = schema.items[i]; |
150
|
|
|
if (options.coerce) |
151
|
|
|
value[i] = options.coerce(value[i], propDef); |
152
|
|
|
errors.concat(checkProp(value[i],propDef,path,i)); |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
if(schema.minItems && value.length < schema.minItems){ |
156
|
|
|
addError("There must be a minimum of " + schema.minItems + " in the array"); |
157
|
|
|
} |
158
|
|
|
if(schema.maxItems && value.length > schema.maxItems){ |
159
|
|
|
addError("There must be a maximum of " + schema.maxItems + " in the array"); |
160
|
|
|
} |
161
|
|
|
}else if(schema.properties || schema.additionalProperties){ |
162
|
|
|
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties)); |
163
|
|
|
} |
164
|
|
|
if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){ |
165
|
|
|
addError("does not match the regex pattern " + schema.pattern); |
166
|
|
|
} |
167
|
|
|
if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){ |
168
|
|
|
addError("may only be " + schema.maxLength + " characters long"); |
169
|
|
|
} |
170
|
|
|
if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){ |
171
|
|
|
addError("must be at least " + schema.minLength + " characters long"); |
172
|
|
|
} |
173
|
|
|
if(typeof schema.minimum !== undefined && typeof value == typeof schema.minimum && |
174
|
|
|
schema.minimum > value){ |
175
|
|
|
addError("must have a minimum value of " + schema.minimum); |
176
|
|
|
} |
177
|
|
|
if(typeof schema.maximum !== undefined && typeof value == typeof schema.maximum && |
178
|
|
|
schema.maximum < value){ |
179
|
|
|
addError("must have a maximum value of " + schema.maximum); |
180
|
|
|
} |
181
|
|
|
if(schema['enum']){ |
182
|
|
|
var enumer = schema['enum']; |
183
|
|
|
l = enumer.length; |
184
|
|
|
var found; |
185
|
|
|
for(var j = 0; j < l; j++){ |
186
|
|
|
if(enumer[j]===value){ |
187
|
|
|
found=1; |
188
|
|
|
break; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
if(!found){ |
192
|
|
|
addError("does not have a value in the enumeration " + enumer.join(", ")); |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
if(typeof schema.maxDecimal == 'number' && |
196
|
|
|
(value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){ |
197
|
|
|
addError("may only have " + schema.maxDecimal + " digits of decimal places"); |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
return null; |
202
|
|
|
} |
203
|
|
|
// validate an object against a schema |
204
|
|
|
function checkObj(instance,objTypeDef,path,additionalProp){ |
205
|
|
|
|
206
|
|
|
if(typeof objTypeDef =='object'){ |
207
|
|
|
if(typeof instance != 'object' || instance instanceof Array){ |
208
|
|
|
errors.push({property:path,message:"an object is required"}); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
for(var i in objTypeDef){ |
212
|
|
|
if(objTypeDef.hasOwnProperty(i)){ |
213
|
|
|
var value = instance[i]; |
214
|
|
|
// skip _not_ specified properties |
215
|
|
|
if (value === undefined && options.existingOnly) continue; |
216
|
|
|
var propDef = objTypeDef[i]; |
217
|
|
|
// set default |
218
|
|
|
if(value === undefined && propDef["default"]){ |
219
|
|
|
value = instance[i] = propDef["default"]; |
220
|
|
|
} |
221
|
|
|
if(options.coerce && i in instance){ |
222
|
|
|
value = instance[i] = options.coerce(value, propDef); |
223
|
|
|
} |
224
|
|
|
checkProp(value,propDef,path,i); |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
for(i in instance){ |
229
|
|
|
if(instance.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && objTypeDef && !objTypeDef[i] && additionalProp===false){ |
230
|
|
|
if (options.filter) { |
231
|
|
|
delete instance[i]; |
232
|
|
|
continue; |
233
|
|
|
} else { |
234
|
|
|
errors.push({property:path,message:(typeof value) + "The property " + i + |
|
|
|
|
235
|
|
|
" is not defined in the schema and the schema does not allow additional properties"}); |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
var requires = objTypeDef && objTypeDef[i] && objTypeDef[i].requires; |
239
|
|
|
if(requires && !(requires in instance)){ |
240
|
|
|
errors.push({property:path,message:"the presence of the property " + i + " requires that " + requires + " also be present"}); |
241
|
|
|
} |
242
|
|
|
value = instance[i]; |
243
|
|
|
if(additionalProp && (!(objTypeDef && typeof objTypeDef == 'object') || !(i in objTypeDef))){ |
244
|
|
|
if(options.coerce){ |
245
|
|
|
value = instance[i] = options.coerce(value, additionalProp); |
246
|
|
|
} |
247
|
|
|
checkProp(value,additionalProp,path,i); |
248
|
|
|
} |
249
|
|
|
if(!_changing && value && value.$schema){ |
250
|
|
|
errors = errors.concat(checkProp(value,value.$schema,path,i)); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
return errors; |
254
|
|
|
} |
255
|
|
|
if(schema){ |
256
|
|
|
checkProp(instance,schema,'',_changing || ''); |
257
|
|
|
} |
258
|
|
|
if(!_changing && instance && instance.$schema){ |
259
|
|
|
checkProp(instance,instance.$schema,'',''); |
260
|
|
|
} |
261
|
|
|
return {valid:!errors.length,errors:errors}; |
262
|
|
|
}; |
263
|
|
|
exports.mustBeValid = function(result){ |
264
|
|
|
// summary: |
265
|
|
|
// This checks to ensure that the result is valid and will throw an appropriate error message if it is not |
266
|
|
|
// result: the result returned from checkPropertyChange or validate |
267
|
|
|
if(!result.valid){ |
268
|
|
|
throw new TypeError(result.errors.map(function(error){return "for property " + error.property + ': ' + error.message;}).join(", \n")); |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return exports; |
273
|
|
|
})); |
274
|
|
|
|