Skip to content

Commit

Permalink
changing ReadOnlyValidator to use boolean property instead of array.
Browse files Browse the repository at this point in the history
Including the concept of read/write modes.
  • Loading branch information
jorgesartori committed Mar 24, 2023
1 parent dcd6749 commit cb248c3
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 102 deletions.
76 changes: 14 additions & 62 deletions src/main/java/com/networknt/schema/ReadOnlyValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,82 +16,34 @@

package com.networknt.schema;

import com.fasterxml.jackson.databind.JsonNode;
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);
if (writeMode) {
return Set.of(buildValidationMessage(at));
} else {
return Set.of();
}
return path;
}

}
}
119 changes: 79 additions & 40 deletions src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@

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;

Expand All @@ -48,7 +50,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;

Expand All @@ -58,38 +61,47 @@ 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>();

/**
* 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>();

Expand All @@ -108,9 +120,11 @@ public void setTypeLoose(boolean typeLoose) {
}

/**
* 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
*/
Expand Down Expand Up @@ -244,6 +258,7 @@ public void setLosslessNarrowing(boolean losslessNarrowing) {

/**
* Indicates whether OpenAPI 3 style discriminators should be supported
*
* @return true in case discriminators are enabled
* @since 1.0.51
*/
Expand All @@ -252,26 +267,36 @@ public boolean isOpenAPI3StyleDiscriminators() {
}

/**
* 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) {
Expand All @@ -293,4 +318,18 @@ public boolean isResetCollectorContext() {
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
*/
public void setWriteMode(boolean writeMode) {
this.writeMode = writeMode;
}
}
60 changes: 60 additions & 0 deletions src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 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()).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);
}

}
4 changes: 4 additions & 0 deletions src/test/resources/data/read-only-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"firstName": "George",
"lastName": "Harrison"
}
15 changes: 15 additions & 0 deletions src/test/resources/schema/read-only-schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

0 comments on commit cb248c3

Please sign in to comment.