Add JSON Schema Validator
[folly.git] / folly / experimental / test / JSONSchemaTest.cpp
1 /*
2  * Copyright 2015 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 <gtest/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, {1, 2, 3, 4, 5}));
161   ASSERT_TRUE(check(schema, {1, 2, 3}));
162   ASSERT_FALSE(check(schema, {1}));
163 }
164
165 TEST(JSONSchemaTest, TestMaximumArrayItems) {
166   dynamic schema = dynamic::object("maxItems", 3);
167   ASSERT_FALSE(check(schema, {1, 2, 3, 4, 5}));
168   ASSERT_TRUE(check(schema, {1, 2, 3}));
169   ASSERT_TRUE(check(schema, {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, {1, 2, 3}));
176   ASSERT_FALSE(check(schema, {1, 2, 3, 1}));
177   ASSERT_FALSE(check(schema, {"cat", "dog", 1, 2, "cat"}));
178   ASSERT_TRUE(check(schema, {
179     dynamic::object("foo", "bar"),
180     dynamic::object("foo", "baz")
181   }));
182
183   schema = dynamic::object("uniqueItems", false);
184   ASSERT_TRUE(check(schema, {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, {2, 3, 4}));
190   ASSERT_FALSE(check(schema, {3, 4, 1}));
191 }
192
193 TEST(JSONSchemaTest, TestArrayAdditionalItems) {
194   dynamic schema = dynamic::object(
195       "items", {dynamic::object("minimum", 2), dynamic::object("minimum", 1)})(
196       "additionalItems", dynamic::object("minimum", 3));
197   ASSERT_TRUE(check(schema, {2, 1, 3, 3, 3, 3, 4}));
198   ASSERT_FALSE(check(schema, {2, 1, 3, 3, 3, 3, 1}));
199 }
200
201 TEST(JSONSchemaTest, TestArrayNoAdditionalItems) {
202   dynamic schema = dynamic::object("items", {dynamic::object("minimum", 2)})(
203       "additionalItems", false);
204   ASSERT_FALSE(check(schema, {3, 3, 3}));
205 }
206
207 TEST(JSONSchemaTest, TestArrayItemsNotPresent) {
208   dynamic schema = dynamic::object("additionalItems", false);
209   ASSERT_TRUE(check(schema, {3, 3, 3}));
210 }
211
212 TEST(JSONSchemaTest, TestRef) {
213   dynamic schema = dynamic::object(
214       "definitions",
215       dynamic::object("positiveInteger",
216                       dynamic::object("minimum", 1)("type", "integer")))(
217       "items", dynamic::object("$ref", "#/definitions/positiveInteger"));
218   ASSERT_TRUE(check(schema, {1, 2, 3, 4}));
219   ASSERT_FALSE(check(schema, {4, -5}));
220 }
221
222 TEST(JSONSchemaTest, TestRecursiveRef) {
223   dynamic schema = dynamic::object(
224       "properties", dynamic::object("more", dynamic::object("$ref", "#")));
225   dynamic d = dynamic::object;
226   ASSERT_TRUE(check(schema, d));
227   d["more"] = dynamic::object;
228   ASSERT_TRUE(check(schema, d));
229   d["more"]["more"] = dynamic::object;
230   ASSERT_TRUE(check(schema, d));
231   d["more"]["more"]["more"] = dynamic::object;
232   ASSERT_TRUE(check(schema, d));
233 }
234
235 TEST(JSONSchemaTest, TestDoubleRecursiveRef) {
236   dynamic schema =
237       dynamic::object("properties",
238                       dynamic::object("more", dynamic::object("$ref", "#"))(
239                           "less", dynamic::object("$ref", "#")));
240   dynamic d = dynamic::object;
241   ASSERT_TRUE(check(schema, d));
242   d["more"] = dynamic::object;
243   d["less"] = dynamic::object;
244   ASSERT_TRUE(check(schema, d));
245   d["more"]["less"] = dynamic::object;
246   d["less"]["mode"] = dynamic::object;
247   ASSERT_TRUE(check(schema, d));
248 }
249
250 TEST(JSONSchemaTest, TestInfinitelyRecursiveRef) {
251   dynamic schema = dynamic::object("not", dynamic::object("$ref", "#"));
252   auto validator = makeValidator(schema);
253   ASSERT_THROW(validator->validate({1, 2}), std::runtime_error);
254 }
255
256 TEST(JSONSchemaTest, TestRequired) {
257   dynamic schema = dynamic::object("required", {"foo", "bar"});
258   ASSERT_FALSE(check(schema, dynamic::object("foo", 123)));
259   ASSERT_FALSE(check(schema, dynamic::object("bar", 123)));
260   ASSERT_TRUE(check(schema, dynamic::object("bar", 123)("foo", 456)));
261 }
262
263 TEST(JSONSchemaTest, TestMinMaxProperties) {
264   dynamic schema = dynamic::object("minProperties", 1)("maxProperties", 3);
265   dynamic d = dynamic::object;
266   ASSERT_FALSE(check(schema, d));
267   d["a"] = 1;
268   ASSERT_TRUE(check(schema, d));
269   d["b"] = 2;
270   ASSERT_TRUE(check(schema, d));
271   d["c"] = 3;
272   ASSERT_TRUE(check(schema, d));
273   d["d"] = 4;
274   ASSERT_FALSE(check(schema, d));
275 }
276
277 TEST(JSONSchemaTest, TestProperties) {
278   dynamic schema = dynamic::object(
279       "properties", dynamic::object("p1", dynamic::object("minimum", 1)))(
280       "patternProperties", dynamic::object("[0-9]+", dynamic::object))(
281       "additionalProperties", dynamic::object("maximum", 5));
282   ASSERT_TRUE(check(schema, dynamic::object("p1", 1)));
283   ASSERT_FALSE(check(schema, dynamic::object("p1", 0)));
284   ASSERT_TRUE(check(schema, dynamic::object("123", "anything")));
285   ASSERT_TRUE(check(schema, dynamic::object("123", 500)));
286   ASSERT_TRUE(check(schema, dynamic::object("other_property", 4)));
287   ASSERT_FALSE(check(schema, dynamic::object("other_property", 6)));
288 }
289 TEST(JSONSchemaTest, TestPropertyAndPattern) {
290   dynamic schema = dynamic::object
291     ("properties", dynamic::object("p1", dynamic::object("minimum", 1)))
292     ("patternProperties", dynamic::object("p.", dynamic::object("maximum", 5)));
293   ASSERT_TRUE(check(schema, dynamic::object("p1", 3)));
294   ASSERT_FALSE(check(schema, dynamic::object("p1", 0)));
295   ASSERT_FALSE(check(schema, dynamic::object("p1", 6)));
296 }
297
298 TEST(JSONSchemaTest, TestPropertyDependency) {
299   dynamic schema =
300       dynamic::object("dependencies", dynamic::object("p1", {"p2"}));
301   ASSERT_TRUE(check(schema, dynamic::object));
302   ASSERT_TRUE(check(schema, dynamic::object("p1", 1)("p2", 1)));
303   ASSERT_FALSE(check(schema, dynamic::object("p1", 1)));
304 }
305
306 TEST(JSONSchemaTest, TestSchemaDependency) {
307   dynamic schema = dynamic::object(
308       "dependencies",
309       dynamic::object("p1", dynamic::object("required", {"p2"})));
310   ASSERT_TRUE(check(schema, dynamic::object));
311   ASSERT_TRUE(check(schema, dynamic::object("p1", 1)("p2", 1)));
312   ASSERT_FALSE(check(schema, dynamic::object("p1", 1)));
313 }
314
315 TEST(JSONSchemaTest, TestEnum) {
316   dynamic schema = dynamic::object("enum", {"a", 1});
317   ASSERT_TRUE(check(schema, "a"));
318   ASSERT_TRUE(check(schema, 1));
319   ASSERT_FALSE(check(schema, "b"));
320 }
321
322 TEST(JSONSchemaTest, TestType) {
323   dynamic schema = dynamic::object("type", "object");
324   ASSERT_TRUE(check(schema, dynamic::object));
325   ASSERT_FALSE(check(schema, dynamic(5)));
326 }
327
328 TEST(JSONSchemaTest, TestTypeArray) {
329   dynamic schema = dynamic::object("type", {"array", "number"});
330   ASSERT_TRUE(check(schema, dynamic(5)));
331   ASSERT_TRUE(check(schema, dynamic(1.1)));
332   ASSERT_FALSE(check(schema, dynamic::object));
333 }
334
335 TEST(JSONSchemaTest, TestAllOf) {
336   dynamic schema = dynamic::object(
337       "allOf",
338       {dynamic::object("minimum", 1), dynamic::object("type", "integer")});
339   ASSERT_TRUE(check(schema, 2));
340   ASSERT_FALSE(check(schema, 0));
341   ASSERT_FALSE(check(schema, 1.1));
342 }
343
344 TEST(JSONSchemaTest, TestAnyOf) {
345   dynamic schema = dynamic::object(
346       "anyOf",
347       {dynamic::object("minimum", 1), dynamic::object("type", "integer")});
348   ASSERT_TRUE(check(schema, 2)); // matches both
349   ASSERT_FALSE(check(schema, 0.1)); // matches neither
350   ASSERT_TRUE(check(schema, 1.1)); // matches first one
351   ASSERT_TRUE(check(schema, 0)); // matches second one
352 }
353
354 TEST(JSONSchemaTest, TestOneOf) {
355   dynamic schema = dynamic::object(
356       "oneOf",
357       {dynamic::object("minimum", 1), dynamic::object("type", "integer")});
358   ASSERT_FALSE(check(schema, 2)); // matches both
359   ASSERT_FALSE(check(schema, 0.1)); // matches neither
360   ASSERT_TRUE(check(schema, 1.1)); // matches first one
361   ASSERT_TRUE(check(schema, 0)); // matches second one
362 }
363
364 TEST(JSONSchemaTest, TestNot) {
365   dynamic schema =
366       dynamic::object("not", dynamic::object("minimum", 5)("maximum", 10));
367   ASSERT_TRUE(check(schema, 4));
368   ASSERT_FALSE(check(schema, 7));
369   ASSERT_TRUE(check(schema, 11));
370 }
371
372 // The tests below use some sample schema from json-schema.org
373
374 TEST(JSONSchemaTest, TestMetaSchema) {
375   const char* example1 =
376       "\
377     { \
378       \"title\": \"Example Schema\", \
379       \"type\": \"object\", \
380       \"properties\": { \
381         \"firstName\": { \
382           \"type\": \"string\" \
383         }, \
384         \"lastName\": { \
385           \"type\": \"string\" \
386         }, \
387         \"age\": { \
388           \"description\": \"Age in years\", \
389           \"type\": \"integer\", \
390           \"minimum\": 0 \
391         } \
392       }, \
393       \"required\": [\"firstName\", \"lastName\"] \
394     }";
395
396   auto val = makeSchemaValidator();
397   val->validate(parseJson(example1)); // doesn't throw
398
399   ASSERT_THROW(val->validate("123"), std::runtime_error);
400 }
401
402 TEST(JSONSchemaTest, TestProductSchema) {
403   const char* productSchema =
404       "\
405   { \
406     \"$schema\": \"http://json-schema.org/draft-04/schema#\", \
407       \"title\": \"Product\", \
408       \"description\": \"A product from Acme's catalog\", \
409       \"type\": \"object\", \
410       \"properties\": { \
411         \"id\": { \
412           \"description\": \"The unique identifier for a product\", \
413           \"type\": \"integer\" \
414         }, \
415         \"name\": { \
416           \"description\": \"Name of the product\", \
417           \"type\": \"string\" \
418         }, \
419         \"price\": { \
420           \"type\": \"number\", \
421           \"minimum\": 0, \
422           \"exclusiveMinimum\": true \
423         }, \
424         \"tags\": { \
425           \"type\": \"array\", \
426           \"items\": { \
427             \"type\": \"string\" \
428           }, \
429           \"minItems\": 1, \
430           \"uniqueItems\": true \
431         } \
432       }, \
433       \"required\": [\"id\", \"name\", \"price\"] \
434   }";
435   const char* product =
436       "\
437   { \
438     \"id\": 1, \
439     \"name\": \"A green door\", \
440     \"price\": 12.50, \
441     \"tags\": [\"home\", \"green\"] \
442   }";
443   ASSERT_TRUE(check(parseJson(productSchema), parseJson(product)));
444 }