/* * Copyright (c) 2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.networknt.schema; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import com.fasterxml.jackson.core.JsonProcessingException; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.output.OutputUnit; import com.networknt.schema.serialization.JsonMapperFactory; /** * OutputUnitTest. * * @see A * Specification for Machine-Readable Output for JSON Schema Validation and * Annotation */ public class OutputUnitTest { String schemaData = "{\r\n" + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + " \"$id\": \"https://json-schema.org/schemas/example\",\r\n" + " \"type\": \"object\",\r\n" + " \"title\": \"root\",\r\n" + " \"properties\": {\r\n" + " \"foo\": {\r\n" + " \"allOf\": [\r\n" + " { \"required\": [\"unspecified-prop\"] },\r\n" + " {\r\n" + " \"type\": \"object\",\r\n" + " \"title\": \"foo-title\",\r\n" + " \"properties\": {\r\n" + " \"foo-prop\": {\r\n" + " \"const\": 1,\r\n" + " \"title\": \"foo-prop-title\"\r\n" + " }\r\n" + " },\r\n" + " \"additionalProperties\": { \"type\": \"boolean\" }\r\n" + " }\r\n" + " ]\r\n" + " },\r\n" + " \"bar\": { \"$ref\": \"#/$defs/bar\" }\r\n" + " },\r\n" + " \"$defs\": {\r\n" + " \"bar\": {\r\n" + " \"type\": \"object\",\r\n" + " \"title\": \"bar-title\",\r\n" + " \"properties\": {\r\n" + " \"bar-prop\": {\r\n" + " \"type\": \"integer\",\r\n" + " \"minimum\": 10,\r\n" + " \"title\": \"bar-prop-title\"\r\n" + " }\r\n" + " }\r\n" + " }\r\n" + " }\r\n" + "}"; String inputData1 = "{\r\n" + " \"foo\": { \"foo-prop\": \"not 1\", \"other-prop\": false },\r\n" + " \"bar\": { \"bar-prop\": 2 }\r\n" + "}"; String inputData2 = "{\r\n" + " \"foo\": {\r\n" + " \"foo-prop\": 1,\r\n" + " \"unspecified-prop\": true\r\n" + " },\r\n" + " \"bar\": { \"bar-prop\": 20 }\r\n" + "}"; @Test void annotationCollectionList() throws JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(schemaData, config); String inputData = inputData1; OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); 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\"}}]}"; assertEquals(expected, output); } @Test void annotationCollectionHierarchical() throws JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(schemaData, config); String inputData = inputData1; OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); 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\"}}]}]}"; assertEquals(expected, output); } @Test void annotationCollectionHierarchical2() throws JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(schemaData, config); String inputData = inputData2; OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); 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\"}}]}]}"; assertEquals(expected, output); } enum FormatInput { DATE_TIME("date-time"), DATE("date"), TIME("time"), DURATION("duration"), EMAIL("email"), IDN_EMAIL("idn-email"), HOSTNAME("hostname"), IDN_HOSTNAME("idn-hostname"), IPV4("ipv4"), IPV6("ipv6"), URI("uri"), URI_REFERENCE("uri-reference"), IRI("iri"), IRI_REFERENCE("iri-reference"), UUID("uuid"), JSON_POINTER("json-pointer"), RELATIVE_JSON_POINTER("relative-json-pointer"), REGEX("regex"); String format; FormatInput(String format) { this.format = format; } } @ParameterizedTest @EnumSource(FormatInput.class) void formatAnnotation(FormatInput formatInput) { String formatSchema = "{\r\n" + " \"type\": \"string\",\r\n" + " \"format\": \""+formatInput.format+"\"\r\n" + "}"; JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(formatSchema, config); OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); assertTrue(outputUnit.isValid()); OutputUnit details = outputUnit.getDetails().get(0); assertEquals(formatInput.format, details.getAnnotations().get("format")); } @ParameterizedTest @EnumSource(FormatInput.class) void formatAssertion(FormatInput formatInput) { String formatSchema = "{\r\n" + " \"type\": \"string\",\r\n" + " \"format\": \""+formatInput.format+"\"\r\n" + "}"; JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(formatSchema, config); OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); executionConfiguration.getExecutionConfig().setFormatAssertionsEnabled(true); }); assertFalse(outputUnit.isValid()); OutputUnit details = outputUnit.getDetails().get(0); assertEquals(formatInput.format, details.getDroppedAnnotations().get("format")); assertNotNull(details.getErrors().get("format")); } @Test void typeUnion() { String typeSchema = "{\r\n" + " \"type\": [\"string\",\"array\"]\r\n" + "}"; JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(typeSchema, config); OutputUnit outputUnit = schema.validate("1", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); assertFalse(outputUnit.isValid()); OutputUnit details = outputUnit.getDetails().get(0); assertNotNull(details.getErrors().get("type")); } @Test void unevaluatedProperties() throws JsonProcessingException { Map external = new HashMap<>(); String externalSchemaData = "{\r\n" + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + " \"$id\": \"https://www.example.org/point.json\",\r\n" + " \"type\": \"object\",\r\n" + " \"required\": [\r\n" + " \"type\",\r\n" + " \"coordinates\"\r\n" + " ],\r\n" + " \"properties\": {\r\n" + " \"type\": {\r\n" + " \"type\": \"string\",\r\n" + " \"enum\": [\r\n" + " \"Point\"\r\n" + " ]\r\n" + " },\r\n" + " \"coordinates\": {\r\n" + " \"type\": \"array\",\r\n" + " \"minItems\": 2,\r\n" + " \"items\": {\r\n" + " \"type\": \"number\"\r\n" + " }\r\n" + " }\r\n" + " }\r\n" + "}"; external.put("https://www.example.org/point.json", externalSchemaData); String schemaData = "{\r\n" + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + " \"$ref\": \"https://www.example.org/point.json\",\r\n" + " \"unevaluatedProperties\": false\r\n" + "}"; JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(external))); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(schemaData, config); // The following checks if the heirarchical output format is correct with multiple unevaluated properties String inputData = "{\r\n" + " \"type\": \"Point\",\r\n" + " \"hello\": \"Point\",\r\n" + " \"world\": \"Point\",\r\n" + " \"coordinates\": [1, 1]\r\n" + "}"; OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> executionContext.getExecutionConfig() .setAnnotationCollectionFilter(keyword -> true)); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); 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\"]}}]}"; assertEquals(expected, output); } /** * Test that anyOf doesn't short circuit if annotations are turned on. * * @see anyOf * @throws JsonProcessingException the exception */ @Test void anyOf() throws JsonProcessingException { // Test that any of doesn't short circuit if annotations need to be collected String schemaData = "{\r\n" + " \"type\": \"object\",\r\n" + " \"anyOf\": [\r\n" + " {\r\n" + " \"properties\": {\r\n" + " \"foo\": {\r\n" + " \"type\": \"string\"\r\n" + " }\r\n" + " }\r\n" + " },\r\n" + " {\r\n" + " \"properties\": {\r\n" + " \"bar\": {\r\n" + " \"type\": \"integer\"\r\n" + " }\r\n" + " }\r\n" + " }\r\n" + " ]\r\n" + "}"; JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(schemaData, config); String inputData = "{\r\n" + " \"foo\": \"hello\",\r\n" + " \"bar\": 1\r\n" + "}"; OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true); executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); 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\"]}}]}"; assertEquals(expected, output); } }