org.mockito
mockito-core
diff --git a/src/main/java/com/networknt/schema/ApplyDefaultsStrategy.java b/src/main/java/com/networknt/schema/ApplyDefaultsStrategy.java
new file mode 100644
index 000000000..4e93826f6
--- /dev/null
+++ b/src/main/java/com/networknt/schema/ApplyDefaultsStrategy.java
@@ -0,0 +1,41 @@
+package com.networknt.schema;
+
+public class ApplyDefaultsStrategy {
+ private final boolean applyPropertyDefaults;
+ private final boolean applyPropertyDefaultsIfNull;
+ private final boolean applyArrayDefaults;
+
+ /**
+ * Specify which default values to apply.
+ * We can apply property defaults only if they are missing or if they are declared to be null in the input json,
+ * and we can apply array defaults if they are declared to be null in the input json.
+ *
+ * Note that the walker changes the input object in place.
+ * If validation fails, the input object will be changed.
+ *
+ * @param applyPropertyDefaults if true then apply defaults inside json objects if the attribute is missing
+ * @param applyPropertyDefaultsIfNull if true then apply defaults inside json objects if the attribute is explicitly null
+ * @param applyArrayDefaults if true then apply defaults inside json arrays if the attribute is explicitly null
+ * @throws IllegalArgumentException if applyPropertyDefaults is false and applyPropertyDefaultsIfNull is true
+ */
+ public ApplyDefaultsStrategy(boolean applyPropertyDefaults, boolean applyPropertyDefaultsIfNull, boolean applyArrayDefaults) {
+ if (!applyPropertyDefaults && applyPropertyDefaultsIfNull) {
+ throw new IllegalArgumentException();
+ }
+ this.applyPropertyDefaults = applyPropertyDefaults;
+ this.applyPropertyDefaultsIfNull = applyPropertyDefaultsIfNull;
+ this.applyArrayDefaults = applyArrayDefaults;
+ }
+
+ public boolean shouldApplyPropertyDefaults() {
+ return applyPropertyDefaults;
+ }
+
+ public boolean shouldApplyPropertyDefaultsIfNull() {
+ return applyPropertyDefaultsIfNull;
+ }
+
+ public boolean shouldApplyArrayDefaults() {
+ return applyArrayDefaults;
+ }
+}
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 3aa9a0b39..6a9da5585 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -38,11 +38,18 @@ public abstract class BaseJsonValidator implements JsonValidator {
private ErrorMessageType errorMessageType;
protected final boolean failFast;
+ protected final ApplyDefaultsStrategy applyDefaultsStrategy;
public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
ValidatorTypeCode validatorType, ValidationContext validationContext) {
- this(schemaPath, schemaNode, parentSchema, validatorType, false,
- validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
+ this.errorMessageType = validatorType;
+ this.schemaPath = schemaPath;
+ this.schemaNode = schemaNode;
+ this.parentSchema = parentSchema;
+ this.validatorType = validatorType;
+ this.suppressSubSchemaRetrieval = false;
+ this.failFast = validationContext.getConfig() != null && validationContext.getConfig().isFailFast();
+ this.applyDefaultsStrategy = validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null;
}
public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
@@ -55,6 +62,7 @@ public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare
this.validatorType = validatorType;
this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval;
this.failFast = failFast;
+ this.applyDefaultsStrategy = null;
}
public String getSchemaPath() {
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java
index 69abc8bab..80c70426a 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator.java
@@ -17,6 +17,7 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
import com.networknt.schema.walk.WalkListenerRunner;
@@ -118,9 +119,21 @@ private void doValidate(Set errors, int i, JsonNode node, Jso
@Override
public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
HashSet validationMessages = new LinkedHashSet();
- if (node != null && node.isArray()) {
+ if (node instanceof ArrayNode) {
+ ArrayNode arrayNode = (ArrayNode) node;
+ JsonNode defaultNode = null;
+ if (applyDefaultsStrategy.shouldApplyArrayDefaults() && schema != null) {
+ defaultNode = schema.getSchemaNode().get("default");
+ if (defaultNode != null && defaultNode.isNull()) {
+ defaultNode = null;
+ }
+ }
int i = 0;
- for (JsonNode n : node) {
+ for (JsonNode n : arrayNode) {
+ if (n.isNull() && defaultNode != null) {
+ arrayNode.set(i, defaultNode);
+ n = defaultNode;
+ }
doWalk(validationMessages, i, n, rootNode, at, shouldValidateSchema);
i++;
}
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index a4de67329..86b5c07fc 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -19,7 +19,6 @@
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
-import java.sql.Ref;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -37,8 +36,6 @@
import com.networknt.schema.walk.JsonSchemaWalker;
import com.networknt.schema.walk.WalkListenerRunner;
-import javax.xml.validation.Schema;
-
/**
* This is the core of json constraint implementation. It parses json constraint
* file and generates JsonValidators. The class is thread safe, once it is
@@ -80,7 +77,7 @@ public JsonSchema(ValidationContext validationContext, URI baseUri, JsonNode sch
private JsonSchema(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode,
JsonSchema parent, boolean suppressSubSchemaRetrieval) {
super(schemaPath, schemaNode, parent, null, suppressSubSchemaRetrieval,
- validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
+ validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
this.validationContext = validationContext;
this.idKeyword = validationContext.getMetaSchema().getIdKeyword();
this.currentUri = this.combineCurrentUriWithIds(currentUri, schemaNode);
@@ -320,7 +317,7 @@ protected ValidationResult validateAndCollect(JsonNode jsonNode, JsonNode rootNo
SchemaValidatorsConfig config = validationContext.getConfig();
// Get the collector context from the thread local.
CollectorContext collectorContext = getCollectorContext();
- // Valdiate.
+ // Validate.
Set errors = validate(jsonNode, rootNode, at);
// When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors.
if (config.doLoadCollectors()) {
@@ -374,7 +371,7 @@ public Set walk(JsonNode node, JsonNode rootNode, String at,
JsonSchemaWalker jsonWalker = entry.getValue();
String schemaPathWithKeyword = entry.getKey();
try {
- // Call all the pre-walk listeners. If atleast one of the pre walk listeners
+ // Call all the pre-walk listeners. If at least one of the pre walk listeners
// returns SKIP, then skip the walk.
if (keywordWalkListenerRunner.runPreWalkListeners(schemaPathWithKeyword,
node,
diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java
index b0e1154a0..d9c9bef68 100644
--- a/src/main/java/com/networknt/schema/PropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertiesValidator.java
@@ -17,6 +17,7 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.walk.DefaultPropertyWalkListenerRunner;
import com.networknt.schema.walk.WalkListenerRunner;
import org.slf4j.Logger;
@@ -52,7 +53,6 @@ public Set validate(JsonNode node, JsonNode rootNode, String
for (Map.Entry entry : schemas.entrySet()) {
JsonSchema propertySchema = entry.getValue();
JsonNode propertyNode = node.get(entry.getKey());
-
if (propertyNode != null) {
// check whether this is a complex validator. save the state
boolean isComplex = state.isComplexValidator();
@@ -117,6 +117,12 @@ private void walkSchema(Map.Entry entry, JsonNode node, Json
boolean shouldValidateSchema, Set validationMessages, WalkListenerRunner propertyWalkListenerRunner) {
JsonSchema propertySchema = entry.getValue();
JsonNode propertyNode = (node == null ? null : node.get(entry.getKey()));
+ if (propertyNode instanceof ObjectNode && applyDefaultsStrategy.shouldApplyPropertyDefaults()) {
+ JsonNode schemaPropertiesNode = propertySchema.getSchemaNode().get("properties");
+ if (schemaPropertiesNode != null) {
+ applyPropertyDefaults((ObjectNode) propertyNode, schemaPropertiesNode, applyDefaultsStrategy.shouldApplyPropertyDefaultsIfNull());
+ }
+ }
boolean executeWalk = propertyWalkListenerRunner.runPreWalkListeners(ValidatorTypeCode.PROPERTIES.getValue(),
propertyNode, rootNode, at + "." + entry.getKey(), propertySchema.getSchemaPath(),
propertySchema.getSchemaNode(), propertySchema.getParentSchema(), validationContext,
@@ -131,6 +137,22 @@ private void walkSchema(Map.Entry entry, JsonNode node, Json
}
+ private static void applyPropertyDefaults(ObjectNode node, JsonNode schemaPropertiesNode, boolean applyPropertyDefaultsIfNull) {
+ for (Iterator> iter = schemaPropertiesNode.fields(); iter.hasNext(); ) {
+ Map.Entry entry = iter.next();
+ String name = entry.getKey();
+ JsonNode propertyNode = node.get(name);
+
+ if (propertyNode == null || (applyPropertyDefaultsIfNull && propertyNode.isNull())) {
+ JsonNode defaultNode = entry.getValue().get("default");
+ if (defaultNode != null && !defaultNode.isNull()) {
+ // mutate the input json
+ node.set(name, defaultNode);
+ }
+ }
+ }
+ }
+
public Map getSchemas() {
return schemas;
}
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index bb935ee35..b822e612f 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -37,6 +37,11 @@ public class SchemaValidatorsConfig {
*/
private boolean failFast;
+ /**
+ * When set to true, walker sets nodes that are missing or NullNode to the default value, if any, and mutate the input json.
+ */
+ private ApplyDefaultsStrategy applyDefaultsStrategy;
+
/**
* When set to true, use ECMA-262 compatible validator
*/
@@ -112,6 +117,14 @@ public boolean isFailFast() {
return this.failFast;
}
+ public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) {
+ this.applyDefaultsStrategy = applyDefaultsStrategy;
+ }
+
+ public ApplyDefaultsStrategy getApplyDefaultsStrategy() {
+ return applyDefaultsStrategy;
+ }
+
public Map getUriMappings() {
// return a copy of the mappings
return new HashMap(uriMappings);
diff --git a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java
new file mode 100644
index 000000000..c86986f2b
--- /dev/null
+++ b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java
@@ -0,0 +1,112 @@
+package com.networknt.schema;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+
+class JsonWalkApplyDefaultsTest {
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false})
+ void testApplyDefaults3(boolean shouldValidateSchema) throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data/walk-data-default.json"));
+ JsonSchema jsonSchema = createSchema(new ApplyDefaultsStrategy(true, true, true));
+ ValidationResult result = jsonSchema.walk(inputNode, shouldValidateSchema);
+ if (shouldValidateSchema) {
+ assertThat(result.getValidationMessages().stream().map(ValidationMessage::getMessage).collect(Collectors.toList()),
+ Matchers.containsInAnyOrder("$.outer.mixedObject.intValue_missingButError: string found, integer expected",
+ "$.outer.badArray[1]: integer found, string expected"));
+ } else {
+ assertThat(result.getValidationMessages(), Matchers.empty());
+ }
+ assertEquals("{\"outer\":{\"mixedObject\":{\"intValue_present\":11,\"intValue_null\":25,\"intValue_missing\":15,\"intValue_missingButError\":\"thirty-five\"},\"goodArray\":[\"hello\",\"five\"],\"badArray\":[\"hello\",5]}}",
+ inputNode.toString());
+ }
+
+ @Test
+ void testApplyDefaults2() throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data/walk-data-default.json"));
+ JsonSchema jsonSchema = createSchema(new ApplyDefaultsStrategy(true, true, false));
+ ValidationResult result = jsonSchema.walk(inputNode, true);
+ assertThat(result.getValidationMessages().stream().map(ValidationMessage::getMessage).collect(Collectors.toList()),
+ Matchers.containsInAnyOrder("$.outer.mixedObject.intValue_missingButError: string found, integer expected",
+ "$.outer.goodArray[1]: null found, string expected",
+ "$.outer.badArray[1]: null found, string expected"));
+ assertEquals("{\"outer\":{\"mixedObject\":{\"intValue_present\":11,\"intValue_null\":25,\"intValue_missing\":15,\"intValue_missingButError\":\"thirty-five\"},\"goodArray\":[\"hello\",null],\"badArray\":[\"hello\",null]}}",
+ inputNode.toString());
+ }
+
+ @Test
+ void testApplyDefaults1() throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data/walk-data-default.json"));
+ JsonSchema jsonSchema = createSchema(new ApplyDefaultsStrategy(true, false, false));
+ ValidationResult result = jsonSchema.walk(inputNode, true);
+ assertThat(result.getValidationMessages().stream().map(ValidationMessage::getMessage).collect(Collectors.toList()),
+ Matchers.containsInAnyOrder("$.outer.mixedObject.intValue_null: null found, integer expected",
+ "$.outer.mixedObject.intValue_missingButError: string found, integer expected",
+ "$.outer.goodArray[1]: null found, string expected",
+ "$.outer.badArray[1]: null found, string expected"));
+ assertEquals("{\"outer\":{\"mixedObject\":{\"intValue_present\":11,\"intValue_null\":null,\"intValue_missing\":15,\"intValue_missingButError\":\"thirty-five\"},\"goodArray\":[\"hello\",null],\"badArray\":[\"hello\",null]}}",
+ inputNode.toString());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "walkWithNoDefaults", "validateWithApplyAllDefaults"} )
+ void testApplyDefaults0(String method) throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data/walk-data-default.json"));
+ JsonNode inputNodeOriginal = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data/walk-data-default.json"));
+ Set validationMessages;
+ switch (method) {
+ case "walkWithNoDefaults": {
+ JsonSchema jsonSchema = createSchema(new ApplyDefaultsStrategy(false, false, false));
+ validationMessages = jsonSchema.walk(inputNode, true).getValidationMessages();
+ break;
+ }
+ case "validateWithApplyAllDefaults": {
+ JsonSchema jsonSchema = createSchema(new ApplyDefaultsStrategy(true, true, true));
+ validationMessages = jsonSchema.validate(inputNode);
+ break;
+ }
+ default: throw new UnsupportedOperationException();
+ }
+ assertThat(validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.toList()),
+ Matchers.containsInAnyOrder("$.outer.mixedObject.intValue_missing: is missing but it is required",
+ "$.outer.mixedObject.intValue_null: null found, integer expected",
+ "$.outer.mixedObject.intValue_missingButError: is missing but it is required",
+ "$.outer.goodArray[1]: null found, string expected",
+ "$.outer.badArray[1]: null found, string expected"));
+ assertEquals(inputNodeOriginal, inputNode);
+ CollectorContext.getInstance().reset(); // necessary because we are calling both jsonSchema.walk and jsonSchema.validate
+ }
+
+ @Test
+ void testIllegalArgumentException() {
+ try {
+ new ApplyDefaultsStrategy(false, true, false);
+ fail("expected IllegalArgumentException");
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+
+ private JsonSchema createSchema(ApplyDefaultsStrategy applyDefaultsStrategy) {
+ JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
+ SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig();
+ schemaValidatorsConfig.setApplyDefaultsStrategy(applyDefaultsStrategy);
+ return schemaFactory.getSchema(getClass().getClassLoader().getResourceAsStream("schema/walk-schema-default.json"), schemaValidatorsConfig);
+ }
+}
diff --git a/src/test/resources/data/walk-data-default.json b/src/test/resources/data/walk-data-default.json
new file mode 100644
index 000000000..3c6da3812
--- /dev/null
+++ b/src/test/resources/data/walk-data-default.json
@@ -0,0 +1,10 @@
+{
+ "outer": {
+ "mixedObject": {
+ "intValue_present": 11,
+ "intValue_null": null
+ },
+ "goodArray": ["hello", null],
+ "badArray": ["hello", null]
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/schema/walk-schema-default.json b/src/test/resources/schema/walk-schema-default.json
new file mode 100644
index 000000000..a47aa93ef
--- /dev/null
+++ b/src/test/resources/schema/walk-schema-default.json
@@ -0,0 +1,64 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Schema with default values ",
+ "type": "object",
+ "properties": {
+ "outer": {
+ "type": "object",
+ "properties": {
+ "mixedObject": {
+ "type": "object",
+ "properties": {
+ "intValue_present": {
+ "description": "the test data supplies a value for this attribute so the default is ignored",
+ "type": "integer",
+ "default": 5
+ },
+ "intValue_missing": {
+ "description": "the test data does not have this attribute, so default should be applied",
+ "type": "integer",
+ "default": 15
+ },
+ "intValue_null": {
+ "description": "the test data supplies the value null for this attribute so the default should be applied",
+ "type": "integer",
+ "default": 25
+ },
+ "intValue_missingButError": {
+ "description": "the test data does not have this attribute, so default should be applied, but the default is wrong so there should be an error",
+ "type": "integer",
+ "default": "thirty-five"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "intValue_present",
+ "intValue_missing",
+ "intValue_null",
+ "intValue_missingButError"
+ ]
+ },
+ "goodArray": {
+ "type": "array",
+ "items": {
+ "description": "if an item in the array is null, then default value should be applied",
+ "type": "string",
+ "default": "five"
+ }
+ },
+ "badArray": {
+ "type": "array",
+ "items": {
+ "description": "if an item in the array is null, then default value should be applied",
+ "type": "string",
+ "default": 5
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": ["mixedObject", "goodArray", "badArray"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["outer"]
+}