• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2024 the original author or authors.
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 package com.networknt.schema;
17 
18 import static org.junit.jupiter.api.Assertions.assertEquals;
19 import static org.junit.jupiter.api.Assertions.assertFalse;
20 import static org.junit.jupiter.api.Assertions.assertNotNull;
21 import static org.junit.jupiter.api.Assertions.assertTrue;
22 
23 import java.util.HashMap;
24 import java.util.Map;
25 
26 import org.junit.jupiter.api.Test;
27 import org.junit.jupiter.params.ParameterizedTest;
28 import org.junit.jupiter.params.provider.EnumSource;
29 
30 import com.fasterxml.jackson.core.JsonProcessingException;
31 import com.networknt.schema.SpecVersion.VersionFlag;
32 import com.networknt.schema.output.OutputUnit;
33 import com.networknt.schema.serialization.JsonMapperFactory;
34 
35 /**
36  * OutputUnitTest.
37  *
38  * @see <a href=
39  *      "https://github.com/json-schema-org/json-schema-spec/blob/main/jsonschema-validation-output-machines.md">A
40  *      Specification for Machine-Readable Output for JSON Schema Validation and
41  *      Annotation</a>
42  */
43 public class OutputUnitTest {
44     String schemaData = "{\r\n"
45             + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
46             + "  \"$id\": \"https://json-schema.org/schemas/example\",\r\n"
47             + "  \"type\": \"object\",\r\n"
48             + "  \"title\": \"root\",\r\n"
49             + "  \"properties\": {\r\n"
50             + "    \"foo\": {\r\n"
51             + "      \"allOf\": [\r\n"
52             + "        { \"required\": [\"unspecified-prop\"] },\r\n"
53             + "        {\r\n"
54             + "          \"type\": \"object\",\r\n"
55             + "          \"title\": \"foo-title\",\r\n"
56             + "          \"properties\": {\r\n"
57             + "            \"foo-prop\": {\r\n"
58             + "              \"const\": 1,\r\n"
59             + "              \"title\": \"foo-prop-title\"\r\n"
60             + "            }\r\n"
61             + "          },\r\n"
62             + "          \"additionalProperties\": { \"type\": \"boolean\" }\r\n"
63             + "        }\r\n"
64             + "      ]\r\n"
65             + "    },\r\n"
66             + "    \"bar\": { \"$ref\": \"#/$defs/bar\" }\r\n"
67             + "  },\r\n"
68             + "  \"$defs\": {\r\n"
69             + "    \"bar\": {\r\n"
70             + "      \"type\": \"object\",\r\n"
71             + "      \"title\": \"bar-title\",\r\n"
72             + "      \"properties\": {\r\n"
73             + "        \"bar-prop\": {\r\n"
74             + "          \"type\": \"integer\",\r\n"
75             + "          \"minimum\": 10,\r\n"
76             + "          \"title\": \"bar-prop-title\"\r\n"
77             + "        }\r\n"
78             + "      }\r\n"
79             + "    }\r\n"
80             + "  }\r\n"
81             + "}";
82 
83     String inputData1 = "{\r\n"
84             + "  \"foo\": { \"foo-prop\": \"not 1\", \"other-prop\": false },\r\n"
85             + "  \"bar\": { \"bar-prop\": 2 }\r\n"
86             + "}";
87 
88     String inputData2 = "{\r\n"
89             + "  \"foo\": {\r\n"
90             + "    \"foo-prop\": 1,\r\n"
91             + "    \"unspecified-prop\": true\r\n"
92             + "  },\r\n"
93             + "  \"bar\": { \"bar-prop\": 20 }\r\n"
94             + "}";
95     @Test
annotationCollectionList()96     void annotationCollectionList() throws JsonProcessingException {
97         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
98         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
99         config.setPathType(PathType.JSON_POINTER);
100         JsonSchema schema = factory.getSchema(schemaData, config);
101 
102         String inputData = inputData1;
103 
104         OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> {
105             executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true);
106             executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
107         });
108         String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit);
109         String expected = "{\"valid\":false,\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/0\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\"instanceLocation\":\"/foo\",\"errors\":{\"required\":\"required property 'unspecified-prop' not found\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"errors\":{\"const\":\"must be the constant value '1'\"},\"droppedAnnotations\":{\"title\":\"foo-prop-title\"}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"errors\":{\"minimum\":\"must have a minimum value of 10\"},\"droppedAnnotations\":{\"title\":\"bar-prop-title\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"droppedAnnotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"other-prop\"]}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"}},{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"}}]}";
110         assertEquals(expected, output);
111     }
112 
113     @Test
annotationCollectionHierarchical()114     void annotationCollectionHierarchical() throws JsonProcessingException {
115         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
116         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
117         config.setPathType(PathType.JSON_POINTER);
118         JsonSchema schema = factory.getSchema(schemaData, config);
119 
120         String inputData = inputData1;
121 
122         OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> {
123             executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true);
124             executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
125         });
126         String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit);
127         String expected = "{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/0\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\"instanceLocation\":\"/foo\",\"errors\":{\"required\":\"required property 'unspecified-prop' not found\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"droppedAnnotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"other-prop\"]},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"errors\":{\"const\":\"must be the constant value '1'\"},\"droppedAnnotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"errors\":{\"minimum\":\"must have a minimum value of 10\"},\"droppedAnnotations\":{\"title\":\"bar-prop-title\"}}]}]}";
128         assertEquals(expected, output);
129     }
130 
131     @Test
annotationCollectionHierarchical2()132     void annotationCollectionHierarchical2() throws JsonProcessingException {
133         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
134         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
135         config.setPathType(PathType.JSON_POINTER);
136         JsonSchema schema = factory.getSchema(schemaData, config);
137 
138         String inputData = inputData2;
139 
140         OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> {
141             executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true);
142             executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
143         });
144         String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit);
145         String expected = "{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"annotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"unspecified-prop\"]},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"annotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"annotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"annotations\":{\"title\":\"bar-prop-title\"}}]}]}";
146         assertEquals(expected, output);
147     }
148 
149     enum FormatInput {
150         DATE_TIME("date-time"),
151         DATE("date"),
152         TIME("time"),
153         DURATION("duration"),
154         EMAIL("email"),
155         IDN_EMAIL("idn-email"),
156         HOSTNAME("hostname"),
157         IDN_HOSTNAME("idn-hostname"),
158         IPV4("ipv4"),
159         IPV6("ipv6"),
160         URI("uri"),
161         URI_REFERENCE("uri-reference"),
162         IRI("iri"),
163         IRI_REFERENCE("iri-reference"),
164         UUID("uuid"),
165         JSON_POINTER("json-pointer"),
166         RELATIVE_JSON_POINTER("relative-json-pointer"),
167         REGEX("regex");
168 
169         String format;
170 
FormatInput(String format)171         FormatInput(String format) {
172             this.format = format;
173         }
174     }
175 
176     @ParameterizedTest
177     @EnumSource(FormatInput.class)
formatAnnotation(FormatInput formatInput)178     void formatAnnotation(FormatInput formatInput) {
179         String formatSchema = "{\r\n"
180                 + "  \"type\": \"string\",\r\n"
181                 + "  \"format\": \""+formatInput.format+"\"\r\n"
182                 + "}";
183         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
184         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
185         config.setPathType(PathType.JSON_POINTER);
186         JsonSchema schema = factory.getSchema(formatSchema, config);
187         OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> {
188             executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true);
189             executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
190         });
191         assertTrue(outputUnit.isValid());
192         OutputUnit details = outputUnit.getDetails().get(0);
193         assertEquals(formatInput.format, details.getAnnotations().get("format"));
194     }
195 
196     @ParameterizedTest
197     @EnumSource(FormatInput.class)
formatAssertion(FormatInput formatInput)198     void formatAssertion(FormatInput formatInput) {
199         String formatSchema = "{\r\n"
200                 + "  \"type\": \"string\",\r\n"
201                 + "  \"format\": \""+formatInput.format+"\"\r\n"
202                 + "}";
203         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
204         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
205         config.setPathType(PathType.JSON_POINTER);
206         JsonSchema schema = factory.getSchema(formatSchema, config);
207         OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> {
208             executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true);
209             executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
210             executionConfiguration.getExecutionConfig().setFormatAssertionsEnabled(true);
211         });
212         assertFalse(outputUnit.isValid());
213         OutputUnit details = outputUnit.getDetails().get(0);
214         assertEquals(formatInput.format, details.getDroppedAnnotations().get("format"));
215         assertNotNull(details.getErrors().get("format"));
216     }
217 
218     @Test
typeUnion()219     void typeUnion() {
220         String typeSchema = "{\r\n"
221                 + "  \"type\": [\"string\",\"array\"]\r\n"
222                 + "}";
223         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
224         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
225         config.setPathType(PathType.JSON_POINTER);
226         JsonSchema schema = factory.getSchema(typeSchema, config);
227         OutputUnit outputUnit = schema.validate("1", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> {
228             executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true);
229             executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
230         });
231         assertFalse(outputUnit.isValid());
232         OutputUnit details = outputUnit.getDetails().get(0);
233         assertNotNull(details.getErrors().get("type"));
234     }
235 
236     @Test
unevaluatedProperties()237     void unevaluatedProperties() throws JsonProcessingException {
238         Map<String, String> external = new HashMap<>();
239 
240         String externalSchemaData = "{\r\n"
241                 + "  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n"
242                 + "  \"$id\": \"https://www.example.org/point.json\",\r\n"
243                 + "  \"type\": \"object\",\r\n"
244                 + "  \"required\": [\r\n"
245                 + "    \"type\",\r\n"
246                 + "    \"coordinates\"\r\n"
247                 + "  ],\r\n"
248                 + "  \"properties\": {\r\n"
249                 + "    \"type\": {\r\n"
250                 + "      \"type\": \"string\",\r\n"
251                 + "      \"enum\": [\r\n"
252                 + "        \"Point\"\r\n"
253                 + "      ]\r\n"
254                 + "    },\r\n"
255                 + "    \"coordinates\": {\r\n"
256                 + "      \"type\": \"array\",\r\n"
257                 + "      \"minItems\": 2,\r\n"
258                 + "      \"items\": {\r\n"
259                 + "        \"type\": \"number\"\r\n"
260                 + "      }\r\n"
261                 + "    }\r\n"
262                 + "  }\r\n"
263                 + "}";
264 
265         external.put("https://www.example.org/point.json", externalSchemaData);
266 
267         String schemaData = "{\r\n"
268                 + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
269                 + "  \"$ref\": \"https://www.example.org/point.json\",\r\n"
270                 + "  \"unevaluatedProperties\": false\r\n"
271                 + "}";
272 
273         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
274                 builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(external)));
275         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
276         config.setPathType(PathType.JSON_POINTER);
277         JsonSchema schema = factory.getSchema(schemaData, config);
278 
279      // The following checks if the heirarchical output format is correct with multiple unevaluated properties
280         String inputData = "{\r\n"
281                 + "  \"type\": \"Point\",\r\n"
282                 + "  \"hello\": \"Point\",\r\n"
283                 + "  \"world\": \"Point\",\r\n"
284                 + "  \"coordinates\": [1, 1]\r\n"
285                 + "}";
286         OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL,
287                 executionContext -> executionContext.getExecutionConfig()
288                         .setAnnotationCollectionFilter(keyword -> true));
289         String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit);
290         String expected = "{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"#\",\"instanceLocation\":\"\",\"errors\":{\"unevaluatedProperties\":[\"property 'hello' is not evaluated and the schema does not allow unevaluated properties\",\"property 'world' is not evaluated and the schema does not allow unevaluated properties\"]},\"droppedAnnotations\":{\"unevaluatedProperties\":[\"hello\",\"world\"]},\"details\":[{\"valid\":false,\"evaluationPath\":\"/$ref\",\"schemaLocation\":\"https://www.example.org/point.json#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"type\",\"coordinates\"]}}]}";
291         assertEquals(expected, output);
292     }
293 
294     /**
295      * Test that anyOf doesn't short circuit if annotations are turned on.
296      *
297      * @see <a href=
298      *      "https://github.com/json-schema-org/json-schema-spec/blob/f8967bcbc6cee27753046f63024b55336a9b1b54/jsonschema-core.md?plain=1#L1717-L1720">anyOf</a>
299      * @throws JsonProcessingException the exception
300      */
301     @Test
anyOf()302     void anyOf() throws JsonProcessingException {
303         // Test that any of doesn't short circuit if annotations need to be collected
304         String schemaData = "{\r\n"
305                 + "  \"type\": \"object\",\r\n"
306                 + "  \"anyOf\": [\r\n"
307                 + "    {\r\n"
308                 + "      \"properties\": {\r\n"
309                 + "        \"foo\": {\r\n"
310                 + "          \"type\": \"string\"\r\n"
311                 + "        }\r\n"
312                 + "      }\r\n"
313                 + "    },\r\n"
314                 + "    {\r\n"
315                 + "      \"properties\": {\r\n"
316                 + "        \"bar\": {\r\n"
317                 + "          \"type\": \"integer\"\r\n"
318                 + "        }\r\n"
319                 + "      }\r\n"
320                 + "    }\r\n"
321                 + "  ]\r\n"
322                 + "}";
323 
324         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
325         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
326         config.setPathType(PathType.JSON_POINTER);
327         JsonSchema schema = factory.getSchema(schemaData, config);
328 
329         String inputData = "{\r\n"
330                 + "    \"foo\": \"hello\",\r\n"
331                 + "    \"bar\": 1\r\n"
332                 + "}";
333         OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> {
334             executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true);
335             executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
336         });
337         String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit);
338         String expected = "{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"#\",\"instanceLocation\":\"\",\"details\":[{\"valid\":true,\"evaluationPath\":\"/anyOf/0\",\"schemaLocation\":\"#/anyOf/0\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"foo\"]}},{\"valid\":true,\"evaluationPath\":\"/anyOf/1\",\"schemaLocation\":\"#/anyOf/1\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"bar\"]}}]}";
339         assertEquals(expected, output);
340     }
341 
342 }
343