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
22using folly::dynamic;
23using folly::parseJson;
24using namespace folly::jsonschema;
25using namespace std;
26
27bool 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
44TEST(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
53TEST(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
65TEST(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
75TEST(JSONSchemaTest, TestMinimumIntExclusive) {
76 dynamic schema = dynamic::object("minimum", 2)("exclusiveMinimum", true);
77 ASSERT_FALSE(check(schema, 2));
78}
79
80TEST(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
90TEST(JSONSchemaTest, TestMaximumIntExclusive) {
91 dynamic schema = dynamic::object("maximum", 2)("exclusiveMaximum", true);
92 ASSERT_FALSE(check(schema, 2));
93}
94
95TEST(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
105TEST(JSONSchemaTest, TestMinimumDoubleExclusive) {
106 dynamic schema = dynamic::object("minimum", 1.75)("exclusiveMinimum", true);
107 ASSERT_FALSE(check(schema, 1.75));
108}
109
110TEST(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
120TEST(JSONSchemaTest, TestMaximumDoubleExclusive) {
121 dynamic schema = dynamic::object("maximum", 12.75)("exclusiveMaximum", true);
122 ASSERT_FALSE(check(schema, 12.75));
123}
124
125TEST(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
138TEST(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
145TEST(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
152TEST(JSONSchemaTest, TestStringPattern) {
153 dynamic schema = dynamic::object("pattern", "[1-9]+");
154 ASSERT_TRUE(check(schema, "123"));
155 ASSERT_FALSE(check(schema, "abc"));
156}
157
158TEST(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
165TEST(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
173TEST(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
187TEST(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
193TEST(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
203TEST(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
210TEST(JSONSchemaTest, TestArrayItemsNotPresent) {
211 dynamic schema = dynamic::object("additionalItems", false);
212 ASSERT_TRUE(check(schema, dynamic::array(3, 3, 3)));
213}
214
215TEST(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
225TEST(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
238TEST(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
253TEST(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
259TEST(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
266TEST(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
280TEST(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}
292TEST(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
302TEST(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
310TEST(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
319TEST(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
326TEST(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
332TEST(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
339TEST(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
349TEST(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
360TEST(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
371TEST(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
381TEST(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
409TEST(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