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