Repository: networknt/json-schema-validator
Pull request #684 (2023-03-27 14:17:41)

Issue683


Patch:
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index d49b79d..e40cf7d 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -142,6 +142,8 @@ public class JsonSchemaFactory {
 
         /**
          * @deprecated Use {@code addUriTranslator} instead.
+         * @param map the map of uri mappings.
+         * @return this builder.
          */
         @Deprecated
         public Builder addUriMappings(final Map<String, String> map) {
diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java
index ec9b85d..45436db 100644
--- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java
+++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java
@@ -16,82 +16,35 @@
 
 package com.networknt.schema;
 
-import com.fasterxml.jackson.databind.JsonNode;
+import java.util.HashSet;
+import java.util.Set;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.*;
+import com.fasterxml.jackson.databind.JsonNode;
 
 public class ReadOnlyValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
 
-    private List<String> fieldNames = new ArrayList<String>();
+    private Boolean writeMode;
 
-    public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
+    public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
+            ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.READ_ONLY, validationContext);
-        if (schemaNode.isArray()) {
-            int size = schemaNode.size();
-            for (int i = 0; i < size; i++) {
-                fieldNames.add(schemaNode.get(i).asText());
-            }
-        }
-
+        this.writeMode = validationContext.getConfig().isWriteMode();
+        String mode = writeMode ? "write mode" : "read mode";
+        logger.debug("Loaded ReadOnlyValidator for property {} as {}", parentSchema, mode);
         parseErrorCode(getValidatorType().getErrorCodeKey());
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
-
-        Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
-
-        for (String fieldName : fieldNames) {
-            JsonNode propertyNode = node.get(fieldName);
-            String datapath = "";
-            if (at.equals("$")) {
-                datapath = datapath + "#original." + fieldName;
-            } else {
-                datapath = datapath + "#original." + at.substring(2) + "." + fieldName;
-            }
-            JsonNode originalNode = getNode(datapath, rootNode);
-
-            boolean theSame = propertyNode != null && originalNode != null && propertyNode.equals(originalNode);
-            if (!theSame) {
-                errors.add(buildValidationMessage(at));
-            }
-        }
-
-        return Collections.unmodifiableSet(errors);
-    }
-
-    private JsonNode getNode(String datapath, JsonNode data) {
-        String path = getSubString(datapath,"$.");
-
-        String[] parts = path.split("\\.");
-        JsonNode result = null;
-        for (int i = 0; i < parts.length; i++) {
-            if (parts[i].contains("[")) {
-                int idx1 = parts[i].indexOf("[");
-                int idx2 = parts[i].indexOf("]");
-                String key = parts[i].substring(0, idx1).trim();
-                int idx = Integer.parseInt(parts[i].substring(idx1 + 1, idx2).trim());
-                result = data.get(key).get(idx);
-            } else {
-                result = data.get(parts[i]);
-            }
-            if (result == null) {
-                break;
-            }
-            data = result;
-        }
-        return result;
-    }
-
-    private String getSubString(String datapath, String keyword){
-        String path = datapath;
-        if (path.startsWith(keyword)) {
-            path = path.substring(2);
-        }
-        return path;
+        Set<ValidationMessage> errors= new HashSet<ValidationMessage>();
+        if (writeMode) {
+        	errors.add(buildValidationMessage(at));
+        } 
+        return errors;
     }
 
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index bdb64a6..07cf5aa 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -29,18 +29,20 @@ import com.networknt.schema.walk.JsonSchemaWalkListener;
 
 public class SchemaValidatorsConfig {
     /**
-     * when validate type, if TYPE_LOOSE = true, will try to convert string to different types to match the type defined in
-     * schema.
+     * when validate type, if TYPE_LOOSE = true, will try to convert string to
+     * different types to match the type defined in schema.
      */
     private boolean typeLoose;
 
     /**
-     * When set to true, validator process is stop immediately when a very first validation error is discovered.
+     * When set to true, validator process is stop immediately when a very first
+     * validation error is discovered.
      */
     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.
+     * 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;
 
@@ -50,7 +52,8 @@ public class SchemaValidatorsConfig {
     private boolean ecma262Validator;
 
     /**
-     * When set to true, use Java-specific semantics rather than native JavaScript semantics
+     * When set to true, use Java-specific semantics rather than native JavaScript
+     * semantics
      */
     private boolean javaSemantics;
 
@@ -60,40 +63,49 @@ public class SchemaValidatorsConfig {
     private boolean losslessNarrowing;
 
     /**
-     * When set to true, support for discriminators is enabled for validations of oneOf, anyOf and allOf as described
-     * on <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">GitHub</a>.
+     * When set to true, support for discriminators is enabled for validations of
+     * oneOf, anyOf and allOf as described on <a href=
+     * "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">GitHub</a>.
      */
     private boolean openAPI3StyleDiscriminators = false;
 
     /**
-     * Map of public, normally internet accessible schema URLs to alternate locations; this allows for offline
-     * validation of schemas that refer to public URLs. This is merged with any mappings the {@link JsonSchemaFactory}
-     * may have been built with.
+     * Map of public, normally internet accessible schema URLs to alternate
+     * locations; this allows for offline validation of schemas that refer to public
+     * URLs. This is merged with any mappings the {@link JsonSchemaFactory} may have
+     * been built with.
      */
     private Map<String, String> uriMappings = new HashMap<String, String>();
 
     private CompositeURITranslator uriTranslators = new CompositeURITranslator();
 
     /**
-     * When a field is set as nullable in the OpenAPI specification, the schema validator validates that it is nullable
-     * however continues with validation against the nullable field
+     * When a field is set as nullable in the OpenAPI specification, the schema
+     * validator validates that it is nullable however continues with validation
+     * against the nullable field
      * <p>
-     * If handleNullableField is set to true && incoming field is nullable && value is field: null --> succeed
-     * If handleNullableField is set to false && incoming field is nullable && value is field: null --> it is up to the type
-     * validator using the SchemaValidator to handle it.
+     * If handleNullableField is set to true && incoming field is nullable && value
+     * is field: null --> succeed If handleNullableField is set to false && incoming
+     * field is nullable && value is field: null --> it is up to the type validator
+     * using the SchemaValidator to handle it.
      */
     private boolean handleNullableField = true;
 
     /**
-     * When set to true resets the {@link CollectorContext} by calling {@link CollectorContext#reset()}.
+     * When set to true resets the {@link CollectorContext} by calling
+     * {@link CollectorContext#reset()}.
      */
     private boolean resetCollectorContext = true;
 
+    /**
+     * When set to true considers that schema is used to write data then ReadOnlyValidator is activated. Default true.
+     */
+    private boolean writeMode = true;
+
     // This is just a constant for listening to all Keywords.
     public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener";
 
-    private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap = new HashMap<String,
-            List<JsonSchemaWalkListener>>();
+    private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap = new HashMap<String, List<JsonSchemaWalkListener>>();
 
     private final List<JsonSchemaWalkListener> propertyWalkListeners = new ArrayList<JsonSchemaWalkListener>();
 
@@ -112,9 +124,11 @@ public class SchemaValidatorsConfig {
     }
 
     /**
-     * When enabled, {@link JsonValidator#validate(JsonNode, JsonNode, String)}
-     * or {@link JsonValidator#validate(JsonNode)} doesn't return any {@link Set}&lt;{@link ValidationMessage}&gt;,
-     * instead a {@link JsonSchemaException} is thrown as soon as a validation errors is discovered.
+     * When enabled, {@link JsonValidator#validate(JsonNode, JsonNode, String)} or
+     * {@link JsonValidator#validate(JsonNode)} doesn't return any
+     * {@link Set}&lt;{@link ValidationMessage}&gt;, instead a
+     * {@link JsonSchemaException} is thrown as soon as a validation errors is
+     * discovered.
      *
      * @param failFast boolean
      */
@@ -147,6 +161,7 @@ public class SchemaValidatorsConfig {
 
     /**
      * @deprecated Use {@code getUriTranslator()} instead
+     * @return Map of public, normally internet accessible schema URLs
      */
     @Deprecated
     public Map<String, String> getUriMappings() {
@@ -156,6 +171,7 @@ public class SchemaValidatorsConfig {
 
     /**
      * @deprecated Use {@code addUriTranslator()} instead
+     * @param uriMappings Map of public, normally internet accessible schema URLs
      */
     @Deprecated
     public void setUriMappings(Map<String, String> uriMappings) {
@@ -267,6 +283,7 @@ public class SchemaValidatorsConfig {
 
     /**
      * Indicates whether OpenAPI 3 style discriminators should be supported
+     * 
      * @return true in case discriminators are enabled
      * @since 1.0.51
      */
@@ -275,26 +292,36 @@ public class SchemaValidatorsConfig {
     }
 
     /**
-     * When enabled, the validation of <code>anyOf</code> and <code>allOf</code> in polymorphism will respect
-     * OpenAPI 3 style discriminators as described in the
-     * <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">OpenAPI 3.0.3 spec</a>.
-     * The presence of a discriminator configuration on the schema will lead to the following changes in the behavior:
+     * When enabled, the validation of <code>anyOf</code> and <code>allOf</code> in
+     * polymorphism will respect OpenAPI 3 style discriminators as described in the
+     * <a href=
+     * "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">OpenAPI
+     * 3.0.3 spec</a>. The presence of a discriminator configuration on the schema
+     * will lead to the following changes in the behavior:
      * <ul>
-     *     <li>for <code>oneOf</code> the spec is unfortunately very vague. Whether <code>oneOf</code> semantics should be
-     *     affected by discriminators or not is not even 100% clear within the members of the OAS steering committee. Therefore
-     *     <code>oneOf</code> at the moment ignores discriminators</li>
-     *     <li>for <code>anyOf</code> the validation will choose one of the candidate schemas for validation based on the
-     *     discriminator property value and will pass validation when this specific schema passes. This is in particular useful
-     *     when the payload could match multiple candidates in the <code>anyOf</code> list and could lead to ambiguity. Example:
-     *     type B has all mandatory properties of A and adds more mandatory ones. Whether the payload is an A or B is determined
-     *     via the discriminator property name. A payload indicating it is an instance of B then requires passing the validation
-     *     of B and passing the validation of A would not be sufficient anymore.</li>
-     *     <li>for <code>allOf</code> use cases with discriminators defined on the copied-in parent type, it is possible to
-     *     automatically validate against a subtype. Example: some schema specifies that there is a field of type A. A carries
-     *     a discriminator field and B inherits from A. Then B is automatically a candidate for validation as well and will be
-     *     chosen in case the discriminator property matches</li>
+     * <li>for <code>oneOf</code> the spec is unfortunately very vague. Whether
+     * <code>oneOf</code> semantics should be affected by discriminators or not is
+     * not even 100% clear within the members of the OAS steering committee.
+     * Therefore <code>oneOf</code> at the moment ignores discriminators</li>
+     * <li>for <code>anyOf</code> the validation will choose one of the candidate
+     * schemas for validation based on the discriminator property value and will
+     * pass validation when this specific schema passes. This is in particular
+     * useful when the payload could match multiple candidates in the
+     * <code>anyOf</code> list and could lead to ambiguity. Example: type B has all
+     * mandatory properties of A and adds more mandatory ones. Whether the payload
+     * is an A or B is determined via the discriminator property name. A payload
+     * indicating it is an instance of B then requires passing the validation of B
+     * and passing the validation of A would not be sufficient anymore.</li>
+     * <li>for <code>allOf</code> use cases with discriminators defined on the
+     * copied-in parent type, it is possible to automatically validate against a
+     * subtype. Example: some schema specifies that there is a field of type A. A
+     * carries a discriminator field and B inherits from A. Then B is automatically
+     * a candidate for validation as well and will be chosen in case the
+     * discriminator property matches</li>
      * </ul>
-     * @param openAPI3StyleDiscriminators whether or not discriminators should be used. Defaults to <code>false</code>
+     * 
+     * @param openAPI3StyleDiscriminators whether or not discriminators should be
+     *                                    used. Defaults to <code>false</code>
      * @since 1.0.51
      */
     public void setOpenAPI3StyleDiscriminators(boolean openAPI3StyleDiscriminators) {
@@ -316,4 +343,18 @@ public class SchemaValidatorsConfig {
     public void setResetCollectorContext(boolean resetCollectorContext) {
         this.resetCollectorContext = resetCollectorContext;
     }
-}
+
+    public boolean isWriteMode() {
+        return writeMode;
+    }
+
+    /**
+     * 
+     * When set to true considers that schema is used to write data then ReadOnlyValidator is activated. Default true.
+     * 
+     * @param writeMode true if schema is used to write data
+     */
+    public void setWriteMode(boolean writeMode) {
+        this.writeMode = writeMode;
+    }
+}
\ No newline at end of file


Test patch:
diff --git a/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java b/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java
new file mode 100644
index 0000000..bb131b5
--- /dev/null
+++ b/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java
@@ -0,0 +1,61 @@
+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.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+class ReadOnlyValidatorTest {
+
+	@Test
+	void givenConfigWriteFalseWhenReadOnlyTrueThenAllows() throws IOException {
+		ObjectNode node = getJsonNode();
+		Set<ValidationMessage> errors = loadJsonSchema(false).validate(node);
+		assertTrue(errors.isEmpty());
+	}
+
+	@Test
+	void givenConfigWriteTrueWhenReadOnlyTrueThenDenies() throws IOException {
+		ObjectNode node = getJsonNode();
+		Set<ValidationMessage> errors = loadJsonSchema(true).validate(node);
+		assertFalse(errors.isEmpty());
+		assertEquals("$.firstName: is a readonly field, it cannot be changed",
+				errors.stream().map(e -> e.getMessage()).collect(Collectors.toList()).get(0));
+	}
+
+	private JsonSchema loadJsonSchema(Boolean write) {
+		JsonSchema schema = this.getJsonSchema(write);
+		schema.initializeValidators();
+		return schema;
+
+	}
+
+	private JsonSchema getJsonSchema(Boolean write) {
+		JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
+		SchemaValidatorsConfig schemaConfig = createSchemaConfig(write);
+		InputStream schema = getClass().getClassLoader().getResourceAsStream("schema/read-only-schema.json");
+		return factory.getSchema(schema, schemaConfig);
+	}
+
+	private SchemaValidatorsConfig createSchemaConfig(Boolean write) {
+		SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+		config.setWriteMode(write);
+		return config;
+	}
+
+	private ObjectNode getJsonNode() throws IOException {
+		InputStream node = getClass().getClassLoader().getResourceAsStream("data/read-only-data.json");
+		ObjectMapper mapper = new ObjectMapper();
+		return (ObjectNode) mapper.readTree(node);
+	}
+
+}
diff --git a/src/test/resources/data/read-only-data.json b/src/test/resources/data/read-only-data.json
new file mode 100644
index 0000000..cee5610
--- /dev/null
+++ b/src/test/resources/data/read-only-data.json
@@ -0,0 +1,4 @@
+{
+	"firstName": "George",
+	"lastName": "Harrison"
+}
diff --git a/src/test/resources/schema/read-only-schema.json b/src/test/resources/schema/read-only-schema.json
new file mode 100644
index 0000000..4b1e3cf
--- /dev/null
+++ b/src/test/resources/schema/read-only-schema.json
@@ -0,0 +1,15 @@
+{
+	"$schema": "https://json-schema.org/draft/2020-12/schema",
+	"title": "Read Only Schema",
+	"description": "Testing Read Only Schema Validation",
+	"type": "object",
+	"properties": {
+		"firstName": {
+			"type": "string",
+			"readOnly": true
+		},
+		"lastName": {
+			"type": "string"
+		}
+	}
+}