1 | /* |
2 | * Copyright 2015-present Facebook, Inc. |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | // Copyright 2004-present Facebook. All Rights Reserved. |
17 | |
18 | #include <folly/experimental/JSONSchema.h> |
19 | #include <folly/json.h> |
20 | #include <folly/portability/GTest.h> |
21 | |
22 | using folly::dynamic; |
23 | using folly::parseJson; |
24 | using namespace folly::jsonschema; |
25 | using namespace std; |
26 | |
27 | bool check(const dynamic& schema, const dynamic& value, bool check = true) { |
28 | if (check) { |
29 | auto schemavalidator = makeSchemaValidator(); |
30 | auto ew = schemavalidator->try_validate(schema); |
31 | if (ew) { |
32 | return false; |
33 | } |
34 | } |
35 | |
36 | auto validator = makeValidator(schema); |
37 | auto ew = validator->try_validate(value); |
38 | if (validator->try_validate(value)) { |
39 | return false; |
40 | } |
41 | return true; |
42 | } |
43 | |
44 | TEST(JSONSchemaTest, TestMultipleOfInt) { |
45 | dynamic schema = dynamic::object("multipleOf" , 2); |
46 | ASSERT_TRUE(check(schema, "invalid" )); |
47 | ASSERT_TRUE(check(schema, 30)); |
48 | ASSERT_TRUE(check(schema, 24.0)); |
49 | ASSERT_FALSE(check(schema, 5)); |
50 | ASSERT_FALSE(check(schema, 2.01)); |
51 | } |
52 | |
53 | TEST(JSONSchemaTest, TestMultipleOfDouble) { |
54 | dynamic schema = dynamic::object("multipleOf" , 1.5); |
55 | ASSERT_TRUE(check(schema, "invalid" )); |
56 | ASSERT_TRUE(check(schema, 30)); |
57 | ASSERT_TRUE(check(schema, 24.0)); |
58 | ASSERT_FALSE(check(schema, 5)); |
59 | ASSERT_FALSE(check(schema, 2.01)); |
60 | |
61 | schema = dynamic::object("multipleOf" , 0.0001); |
62 | ASSERT_TRUE(check(schema, 0.0075)); |
63 | } |
64 | |
65 | TEST(JSONSchemaTest, TestMinimumIntInclusive) { |
66 | dynamic schema = dynamic::object("minimum" , 2); |
67 | ASSERT_TRUE(check(schema, "invalid" )); |
68 | ASSERT_TRUE(check(schema, 30)); |
69 | ASSERT_TRUE(check(schema, 24.0)); |
70 | ASSERT_TRUE(check(schema, 2)); |
71 | ASSERT_FALSE(check(schema, 1)); |
72 | ASSERT_FALSE(check(schema, 1.9999)); |
73 | } |
74 | |
75 | TEST(JSONSchemaTest, TestMinimumIntExclusive) { |
76 | dynamic schema = dynamic::object("minimum" , 2)("exclusiveMinimum" , true); |
77 | ASSERT_FALSE(check(schema, 2)); |
78 | } |
79 | |
80 | TEST(JSONSchemaTest, TestMaximumIntInclusive) { |
81 | dynamic schema = dynamic::object("maximum" , 12); |
82 | ASSERT_TRUE(check(schema, "invalid" )); |
83 | ASSERT_TRUE(check(schema, 3)); |
84 | ASSERT_TRUE(check(schema, 3.1)); |
85 | ASSERT_TRUE(check(schema, 12)); |
86 | ASSERT_FALSE(check(schema, 13)); |
87 | ASSERT_FALSE(check(schema, 12.0001)); |
88 | } |
89 | |
90 | TEST(JSONSchemaTest, TestMaximumIntExclusive) { |
91 | dynamic schema = dynamic::object("maximum" , 2)("exclusiveMaximum" , true); |
92 | ASSERT_FALSE(check(schema, 2)); |
93 | } |
94 | |
95 | TEST(JSONSchemaTest, TestMinimumDoubleInclusive) { |
96 | dynamic schema = dynamic::object("minimum" , 1.75); |
97 | ASSERT_TRUE(check(schema, "invalid" )); |
98 | ASSERT_TRUE(check(schema, 30)); |
99 | ASSERT_TRUE(check(schema, 24.0)); |
100 | ASSERT_TRUE(check(schema, 1.75)); |
101 | ASSERT_FALSE(check(schema, 1)); |
102 | ASSERT_FALSE(check(schema, 1.74)); |
103 | } |
104 | |
105 | TEST(JSONSchemaTest, TestMinimumDoubleExclusive) { |
106 | dynamic schema = dynamic::object("minimum" , 1.75)("exclusiveMinimum" , true); |
107 | ASSERT_FALSE(check(schema, 1.75)); |
108 | } |
109 | |
110 | TEST(JSONSchemaTest, TestMaximumDoubleInclusive) { |
111 | dynamic schema = dynamic::object("maximum" , 12.75); |
112 | ASSERT_TRUE(check(schema, "invalid" )); |
113 | ASSERT_TRUE(check(schema, 3)); |
114 | ASSERT_TRUE(check(schema, 3.1)); |
115 | ASSERT_TRUE(check(schema, 12.75)); |
116 | ASSERT_FALSE(check(schema, 13)); |
117 | ASSERT_FALSE(check(schema, 12.76)); |
118 | } |
119 | |
120 | TEST(JSONSchemaTest, TestMaximumDoubleExclusive) { |
121 | dynamic schema = dynamic::object("maximum" , 12.75)("exclusiveMaximum" , true); |
122 | ASSERT_FALSE(check(schema, 12.75)); |
123 | } |
124 | |
125 | TEST(JSONSchemaTest, TestInvalidSchema) { |
126 | dynamic schema = dynamic::object("multipleOf" , "invalid" ); |
127 | // don't check the schema since it's meant to be invalid |
128 | ASSERT_TRUE(check(schema, 30, false)); |
129 | |
130 | schema = dynamic::object("minimum" , "invalid" )("maximum" , "invalid" ); |
131 | ASSERT_TRUE(check(schema, 2, false)); |
132 | |
133 | schema = dynamic::object("minLength" , "invalid" )("maxLength" , "invalid" ); |
134 | ASSERT_TRUE(check(schema, 2, false)); |
135 | ASSERT_TRUE(check(schema, "foo" , false)); |
136 | } |
137 | |
138 | TEST(JSONSchemaTest, TestMinimumStringLength) { |
139 | dynamic schema = dynamic::object("minLength" , 3); |
140 | ASSERT_TRUE(check(schema, "abcde" )); |
141 | ASSERT_TRUE(check(schema, "abc" )); |
142 | ASSERT_FALSE(check(schema, "a" )); |
143 | } |
144 | |
145 | TEST(JSONSchemaTest, TestMaximumStringLength) { |
146 | dynamic schema = dynamic::object("maxLength" , 3); |
147 | ASSERT_FALSE(check(schema, "abcde" )); |
148 | ASSERT_TRUE(check(schema, "abc" )); |
149 | ASSERT_TRUE(check(schema, "a" )); |
150 | } |
151 | |
152 | TEST(JSONSchemaTest, TestStringPattern) { |
153 | dynamic schema = dynamic::object("pattern" , "[1-9]+" ); |
154 | ASSERT_TRUE(check(schema, "123" )); |
155 | ASSERT_FALSE(check(schema, "abc" )); |
156 | } |
157 | |
158 | TEST(JSONSchemaTest, TestMinimumArrayItems) { |
159 | dynamic schema = dynamic::object("minItems" , 3); |
160 | ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3, 4, 5))); |
161 | ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3))); |
162 | ASSERT_FALSE(check(schema, dynamic::array(1))); |
163 | } |
164 | |
165 | TEST(JSONSchemaTest, TestMaximumArrayItems) { |
166 | dynamic schema = dynamic::object("maxItems" , 3); |
167 | ASSERT_FALSE(check(schema, dynamic::array(1, 2, 3, 4, 5))); |
168 | ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3))); |
169 | ASSERT_TRUE(check(schema, dynamic::array(1))); |
170 | ASSERT_TRUE(check(schema, "foobar" )); |
171 | } |
172 | |
173 | TEST(JSONSchemaTest, TestArrayUniqueItems) { |
174 | dynamic schema = dynamic::object("uniqueItems" , true); |
175 | ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3))); |
176 | ASSERT_FALSE(check(schema, dynamic::array(1, 2, 3, 1))); |
177 | ASSERT_FALSE(check(schema, dynamic::array("cat" , "dog" , 1, 2, "cat" ))); |
178 | ASSERT_TRUE(check( |
179 | schema, |
180 | dynamic::array( |
181 | dynamic::object("foo" , "bar" ), dynamic::object("foo" , "baz" )))); |
182 | |
183 | schema = dynamic::object("uniqueItems" , false); |
184 | ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3, 1))); |
185 | } |
186 | |
187 | TEST(JSONSchemaTest, TestArrayItems) { |
188 | dynamic schema = dynamic::object("items" , dynamic::object("minimum" , 2)); |
189 | ASSERT_TRUE(check(schema, dynamic::array(2, 3, 4))); |
190 | ASSERT_FALSE(check(schema, dynamic::array(3, 4, 1))); |
191 | } |
192 | |
193 | TEST(JSONSchemaTest, TestArrayAdditionalItems) { |
194 | dynamic schema = dynamic::object( |
195 | "items" , |
196 | dynamic::array( |
197 | dynamic::object("minimum" , 2), dynamic::object("minimum" , 1)))( |
198 | "additionalItems" , dynamic::object("minimum" , 3)); |
199 | ASSERT_TRUE(check(schema, dynamic::array(2, 1, 3, 3, 3, 3, 4))); |
200 | ASSERT_FALSE(check(schema, dynamic::array(2, 1, 3, 3, 3, 3, 1))); |
201 | } |
202 | |
203 | TEST(JSONSchemaTest, TestArrayNoAdditionalItems) { |
204 | dynamic schema = |
205 | dynamic::object("items" , dynamic::array(dynamic::object("minimum" , 2)))( |
206 | "additionalItems" , false); |
207 | ASSERT_FALSE(check(schema, dynamic::array(3, 3, 3))); |
208 | } |
209 | |
210 | TEST(JSONSchemaTest, TestArrayItemsNotPresent) { |
211 | dynamic schema = dynamic::object("additionalItems" , false); |
212 | ASSERT_TRUE(check(schema, dynamic::array(3, 3, 3))); |
213 | } |
214 | |
215 | TEST(JSONSchemaTest, TestRef) { |
216 | dynamic schema = dynamic::object( |
217 | "definitions" , |
218 | dynamic::object( |
219 | "positiveInteger" , dynamic::object("minimum" , 1)("type" , "integer" )))( |
220 | "items" , dynamic::object("$ref" , "#/definitions/positiveInteger" )); |
221 | ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3, 4))); |
222 | ASSERT_FALSE(check(schema, dynamic::array(4, -5))); |
223 | } |
224 | |
225 | TEST(JSONSchemaTest, TestRecursiveRef) { |
226 | dynamic schema = dynamic::object( |
227 | "properties" , dynamic::object("more" , dynamic::object("$ref" , "#" ))); |
228 | dynamic d = dynamic::object; |
229 | ASSERT_TRUE(check(schema, d)); |
230 | d["more" ] = dynamic::object; |
231 | ASSERT_TRUE(check(schema, d)); |
232 | d["more" ]["more" ] = dynamic::object; |
233 | ASSERT_TRUE(check(schema, d)); |
234 | d["more" ]["more" ]["more" ] = dynamic::object; |
235 | ASSERT_TRUE(check(schema, d)); |
236 | } |
237 | |
238 | TEST(JSONSchemaTest, TestDoubleRecursiveRef) { |
239 | dynamic schema = dynamic::object( |
240 | "properties" , |
241 | dynamic::object("more" , dynamic::object("$ref" , "#" ))( |
242 | "less" , dynamic::object("$ref" , "#" ))); |
243 | dynamic d = dynamic::object; |
244 | ASSERT_TRUE(check(schema, d)); |
245 | d["more" ] = dynamic::object; |
246 | d["less" ] = dynamic::object; |
247 | ASSERT_TRUE(check(schema, d)); |
248 | d["more" ]["less" ] = dynamic::object; |
249 | d["less" ]["mode" ] = dynamic::object; |
250 | ASSERT_TRUE(check(schema, d)); |
251 | } |
252 | |
253 | TEST(JSONSchemaTest, TestInfinitelyRecursiveRef) { |
254 | dynamic schema = dynamic::object("not" , dynamic::object("$ref" , "#" )); |
255 | auto validator = makeValidator(schema); |
256 | ASSERT_THROW(validator->validate(dynamic::array(1, 2)), std::runtime_error); |
257 | } |
258 | |
259 | TEST(JSONSchemaTest, TestRequired) { |
260 | dynamic schema = dynamic::object("required" , dynamic::array("foo" , "bar" )); |
261 | ASSERT_FALSE(check(schema, dynamic::object("foo" , 123))); |
262 | ASSERT_FALSE(check(schema, dynamic::object("bar" , 123))); |
263 | ASSERT_TRUE(check(schema, dynamic::object("bar" , 123)("foo" , 456))); |
264 | } |
265 | |
266 | TEST(JSONSchemaTest, TestMinMaxProperties) { |
267 | dynamic schema = dynamic::object("minProperties" , 1)("maxProperties" , 3); |
268 | dynamic d = dynamic::object; |
269 | ASSERT_FALSE(check(schema, d)); |
270 | d["a" ] = 1; |
271 | ASSERT_TRUE(check(schema, d)); |
272 | d["b" ] = 2; |
273 | ASSERT_TRUE(check(schema, d)); |
274 | d["c" ] = 3; |
275 | ASSERT_TRUE(check(schema, d)); |
276 | d["d" ] = 4; |
277 | ASSERT_FALSE(check(schema, d)); |
278 | } |
279 | |
280 | TEST(JSONSchemaTest, TestProperties) { |
281 | dynamic schema = dynamic::object( |
282 | "properties" , dynamic::object("p1" , dynamic::object("minimum" , 1)))( |
283 | "patternProperties" , dynamic::object("[0-9]+" , dynamic::object))( |
284 | "additionalProperties" , dynamic::object("maximum" , 5)); |
285 | ASSERT_TRUE(check(schema, dynamic::object("p1" , 1))); |
286 | ASSERT_FALSE(check(schema, dynamic::object("p1" , 0))); |
287 | ASSERT_TRUE(check(schema, dynamic::object("123" , "anything" ))); |
288 | ASSERT_TRUE(check(schema, dynamic::object("123" , 500))); |
289 | ASSERT_TRUE(check(schema, dynamic::object("other_property" , 4))); |
290 | ASSERT_FALSE(check(schema, dynamic::object("other_property" , 6))); |
291 | } |
292 | TEST(JSONSchemaTest, TestPropertyAndPattern) { |
293 | dynamic schema = dynamic::object( |
294 | "properties" , dynamic::object("p1" , dynamic::object("minimum" , 1)))( |
295 | "patternProperties" , |
296 | dynamic::object("p." , dynamic::object("maximum" , 5))); |
297 | ASSERT_TRUE(check(schema, dynamic::object("p1" , 3))); |
298 | ASSERT_FALSE(check(schema, dynamic::object("p1" , 0))); |
299 | ASSERT_FALSE(check(schema, dynamic::object("p1" , 6))); |
300 | } |
301 | |
302 | TEST(JSONSchemaTest, TestPropertyDependency) { |
303 | dynamic schema = dynamic::object( |
304 | "dependencies" , dynamic::object("p1" , dynamic::array("p2" ))); |
305 | ASSERT_TRUE(check(schema, dynamic::object)); |
306 | ASSERT_TRUE(check(schema, dynamic::object("p1" , 1)("p2" , 1))); |
307 | ASSERT_FALSE(check(schema, dynamic::object("p1" , 1))); |
308 | } |
309 | |
310 | TEST(JSONSchemaTest, TestSchemaDependency) { |
311 | dynamic schema = dynamic::object( |
312 | "dependencies" , |
313 | dynamic::object("p1" , dynamic::object("required" , dynamic::array("p2" )))); |
314 | ASSERT_TRUE(check(schema, dynamic::object)); |
315 | ASSERT_TRUE(check(schema, dynamic::object("p1" , 1)("p2" , 1))); |
316 | ASSERT_FALSE(check(schema, dynamic::object("p1" , 1))); |
317 | } |
318 | |
319 | TEST(JSONSchemaTest, TestEnum) { |
320 | dynamic schema = dynamic::object("enum" , dynamic::array("a" , 1)); |
321 | ASSERT_TRUE(check(schema, "a" )); |
322 | ASSERT_TRUE(check(schema, 1)); |
323 | ASSERT_FALSE(check(schema, "b" )); |
324 | } |
325 | |
326 | TEST(JSONSchemaTest, TestType) { |
327 | dynamic schema = dynamic::object("type" , "object" ); |
328 | ASSERT_TRUE(check(schema, dynamic::object)); |
329 | ASSERT_FALSE(check(schema, dynamic(5))); |
330 | } |
331 | |
332 | TEST(JSONSchemaTest, TestTypeArray) { |
333 | dynamic schema = dynamic::object("type" , dynamic::array("array" , "number" )); |
334 | ASSERT_TRUE(check(schema, dynamic(5))); |
335 | ASSERT_TRUE(check(schema, dynamic(1.1))); |
336 | ASSERT_FALSE(check(schema, dynamic::object)); |
337 | } |
338 | |
339 | TEST(JSONSchemaTest, TestAllOf) { |
340 | dynamic schema = dynamic::object( |
341 | "allOf" , |
342 | dynamic::array( |
343 | dynamic::object("minimum" , 1), dynamic::object("type" , "integer" ))); |
344 | ASSERT_TRUE(check(schema, 2)); |
345 | ASSERT_FALSE(check(schema, 0)); |
346 | ASSERT_FALSE(check(schema, 1.1)); |
347 | } |
348 | |
349 | TEST(JSONSchemaTest, TestAnyOf) { |
350 | dynamic schema = dynamic::object( |
351 | "anyOf" , |
352 | dynamic::array( |
353 | dynamic::object("minimum" , 1), dynamic::object("type" , "integer" ))); |
354 | ASSERT_TRUE(check(schema, 2)); // matches both |
355 | ASSERT_FALSE(check(schema, 0.1)); // matches neither |
356 | ASSERT_TRUE(check(schema, 1.1)); // matches first one |
357 | ASSERT_TRUE(check(schema, 0)); // matches second one |
358 | } |
359 | |
360 | TEST(JSONSchemaTest, TestOneOf) { |
361 | dynamic schema = dynamic::object( |
362 | "oneOf" , |
363 | dynamic::array( |
364 | dynamic::object("minimum" , 1), dynamic::object("type" , "integer" ))); |
365 | ASSERT_FALSE(check(schema, 2)); // matches both |
366 | ASSERT_FALSE(check(schema, 0.1)); // matches neither |
367 | ASSERT_TRUE(check(schema, 1.1)); // matches first one |
368 | ASSERT_TRUE(check(schema, 0)); // matches second one |
369 | } |
370 | |
371 | TEST(JSONSchemaTest, TestNot) { |
372 | dynamic schema = |
373 | dynamic::object("not" , dynamic::object("minimum" , 5)("maximum" , 10)); |
374 | ASSERT_TRUE(check(schema, 4)); |
375 | ASSERT_FALSE(check(schema, 7)); |
376 | ASSERT_TRUE(check(schema, 11)); |
377 | } |
378 | |
379 | // The tests below use some sample schema from json-schema.org |
380 | |
381 | TEST(JSONSchemaTest, TestMetaSchema) { |
382 | const char* example1 = |
383 | "\ |
384 | { \ |
385 | \"title\": \"Example Schema\", \ |
386 | \"type\": \"object\", \ |
387 | \"properties\": { \ |
388 | \"firstName\": { \ |
389 | \"type\": \"string\" \ |
390 | }, \ |
391 | \"lastName\": { \ |
392 | \"type\": \"string\" \ |
393 | }, \ |
394 | \"age\": { \ |
395 | \"description\": \"Age in years\", \ |
396 | \"type\": \"integer\", \ |
397 | \"minimum\": 0 \ |
398 | } \ |
399 | }, \ |
400 | \"required\": [\"firstName\", \"lastName\"] \ |
401 | }" ; |
402 | |
403 | auto val = makeSchemaValidator(); |
404 | val->validate(parseJson(example1)); // doesn't throw |
405 | |
406 | ASSERT_THROW(val->validate("123" ), std::runtime_error); |
407 | } |
408 | |
409 | TEST(JSONSchemaTest, TestProductSchema) { |
410 | const char* productSchema = |
411 | "\ |
412 | { \ |
413 | \"$schema\": \"http://json-schema.org/draft-04/schema#\", \ |
414 | \"title\": \"Product\", \ |
415 | \"description\": \"A product from Acme's catalog\", \ |
416 | \"type\": \"object\", \ |
417 | \"properties\": { \ |
418 | \"id\": { \ |
419 | \"description\": \"The unique identifier for a product\", \ |
420 | \"type\": \"integer\" \ |
421 | }, \ |
422 | \"name\": { \ |
423 | \"description\": \"Name of the product\", \ |
424 | \"type\": \"string\" \ |
425 | }, \ |
426 | \"price\": { \ |
427 | \"type\": \"number\", \ |
428 | \"minimum\": 0, \ |
429 | \"exclusiveMinimum\": true \ |
430 | }, \ |
431 | \"tags\": { \ |
432 | \"type\": \"array\", \ |
433 | \"items\": { \ |
434 | \"type\": \"string\" \ |
435 | }, \ |
436 | \"minItems\": 1, \ |
437 | \"uniqueItems\": true \ |
438 | } \ |
439 | }, \ |
440 | \"required\": [\"id\", \"name\", \"price\"] \ |
441 | }" ; |
442 | const char* product = |
443 | "\ |
444 | { \ |
445 | \"id\": 1, \ |
446 | \"name\": \"A green door\", \ |
447 | \"price\": 12.50, \ |
448 | \"tags\": [\"home\", \"green\"] \ |
449 | }" ; |
450 | ASSERT_TRUE(check(parseJson(productSchema), parseJson(product))); |
451 | } |
452 | |