From d353623bf41d1afa875e0f10e1dcb88d99774cca Mon Sep 17 00:00:00 2001 From: fdutton Date: Wed, 14 Jun 2023 08:55:01 -0400 Subject: [PATCH] Adds support for writeOnly (#823) Resolves #798 Co-authored-by: Faron Dutton --- .../networknt/schema/ReadOnlyValidator.java | 20 ++++---- .../schema/SchemaValidatorsConfig.java | 39 ++++++++++++++-- .../networknt/schema/ValidatorTypeCode.java | 3 +- .../networknt/schema/WriteOnlyValidator.java | 34 ++++++++++++++ src/main/resources/jsv-messages.properties | 1 + .../schema/AbstractJsonSchemaTestSuite.java | 13 +++++- .../schema/ReadOnlyValidatorTest.java | 2 +- src/test/resources/draft2020-12/issue798.json | 46 +++++++++++++++++++ 8 files changed, 140 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/networknt/schema/WriteOnlyValidator.java create mode 100644 src/test/resources/draft2020-12/issue798.json diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index 45436db87..2981d7dd6 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -24,24 +24,24 @@ import com.fasterxml.jackson.databind.JsonNode; -public class ReadOnlyValidator extends BaseJsonValidator implements JsonValidator { - private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class); +public class ReadOnlyValidator extends BaseJsonValidator { + private static final Logger logger = LoggerFactory.getLogger(ReadOnlyValidator.class); - private Boolean writeMode; + private final boolean readOnly; - 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); - this.writeMode = validationContext.getConfig().isWriteMode(); - String mode = writeMode ? "write mode" : "read mode"; - logger.debug("Loaded ReadOnlyValidator for property {} as {}", parentSchema, mode); + + this.readOnly = validationContext.getConfig().isReadOnly(); + logger.debug("Loaded ReadOnlyValidator for property {} as {}", parentSchema, "read mode"); parseErrorCode(getValidatorType().getErrorCodeKey()); } + @Override public Set validate(JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors= new HashSet(); - if (writeMode) { + Set errors= new HashSet<>(); + if (this.readOnly) { errors.add(buildValidationMessage(at)); } return errors; diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 044a8f91a..e75553f9a 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -110,9 +110,14 @@ public class SchemaValidatorsConfig { private boolean resetCollectorContext = true; /** - * When set to true considers that schema is used to write data then ReadOnlyValidator is activated. Default true. + * When set to true assumes that schema is used to validate incoming data from an API. */ - private boolean writeMode = true; + private Boolean readOnly = null; + + /** + * When set to true assumes that schema is used to to validate outgoing data from an API. + */ + private Boolean writeOnly = null; /** * The approach used to generate paths in reported messages, logs and errors. Default is the legacy "JSONPath-like" approach. @@ -437,8 +442,28 @@ public void setResetCollectorContext(boolean resetCollectorContext) { this.resetCollectorContext = resetCollectorContext; } + public boolean isReadOnly() { + return null != this.readOnly && this.readOnly; + } + + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + public boolean isWriteOnly() { + return null != this.writeOnly && this.writeOnly; + } + + public void setWriteOnly(boolean writeOnly) { + this.writeOnly = writeOnly; + } + + /** + * Use {@code isReadOnly} or {@code isWriteOnly} + */ + @Deprecated public boolean isWriteMode() { - return this.writeMode; + return null == this.writeOnly || this.writeOnly; } /** @@ -446,9 +471,15 @@ public boolean isWriteMode() { * 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 + * @deprecated Use {@code setReadOnly} or {@code setWriteOnly} */ + @Deprecated public void setWriteMode(boolean writeMode) { - this.writeMode = writeMode; + if (writeMode) { + setWriteOnly(true); + } else { + setReadOnly(true); + } } /** diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index 42398ead2..66be96372 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -93,7 +93,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { PREFIX_ITEMS("prefixItems", "1048", PrefixItemsValidator.class, VersionCode.MinV202012), PROPERTIES("properties", "1025", PropertiesValidator.class, VersionCode.AllVersions), PROPERTYNAMES("propertyNames", "1044", PropertyNamesValidator.class, VersionCode.MinV6), - READ_ONLY("readOnly", "1032", ReadOnlyValidator.class, VersionCode.AllVersions), + READ_ONLY("readOnly", "1032", ReadOnlyValidator.class, VersionCode.MinV7), REF("$ref", "1026", RefValidator.class, VersionCode.AllVersions), REQUIRED("required", "1028", RequiredValidator.class, VersionCode.AllVersions), TRUE("true", "1040", TrueValidator.class, VersionCode.MinV6), @@ -103,6 +103,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { UNION_TYPE("unionType", "1030", UnionTypeValidator.class, VersionCode.AllVersions), UNIQUE_ITEMS("uniqueItems", "1031", UniqueItemsValidator.class, VersionCode.AllVersions), UUID("uuid", "1035", null, VersionCode.AllVersions), + WRITE_ONLY("writeOnly", "1027", WriteOnlyValidator.class, VersionCode.MinV7), ; private static final Map CONSTANTS = new HashMap<>(); diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java new file mode 100644 index 000000000..b86bab57a --- /dev/null +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -0,0 +1,34 @@ +package com.networknt.schema; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +public class WriteOnlyValidator extends BaseJsonValidator { + private static final Logger logger = LoggerFactory.getLogger(WriteOnlyValidator.class); + + private final boolean writeOnly; + + public WriteOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.WRITE_ONLY, validationContext); + + this.writeOnly = validationContext.getConfig().isWriteOnly(); + logger.debug("Loaded WriteOnlyValidator for property {} as {}", parentSchema, "write mode"); + parseErrorCode(getValidatorType().getErrorCodeKey()); + } + + @Override + public Set validate(JsonNode node, JsonNode rootNode, String at) { + debug(logger, node, rootNode, at); + Set errors= new HashSet<>(); + if (this.writeOnly) { + errors.add(buildValidationMessage(at)); + } + return errors; + } + +} diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 99475125a..f180cb232 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -47,3 +47,4 @@ unevaluatedProperties = There are unevaluated properties at the following paths unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique uuid = {0}: {1} is an invalid {2} +writeOnly = {0}: is a write-only field, it cannot appear in the data diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index fdf41ac10..5d83cc516 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -132,8 +132,17 @@ private DynamicNode buildTest(JsonSchemaFactory validatorFactory, TestSpec testS config.setTypeLoose(typeLoose); config.setEcma262Validator(TestSpec.RegexKind.JDK != testSpec.getRegex()); testSpec.getStrictness().forEach(config::setStrict); - if (testSpec.getConfig() != null && testSpec.getConfig().containsKey("isCustomMessageSupported")) { - config.setCustomMessageSupported((Boolean) testSpec.getConfig().get("isCustomMessageSupported")); + + if (testSpec.getConfig() != null) { + if (testSpec.getConfig().containsKey("isCustomMessageSupported")) { + config.setCustomMessageSupported((Boolean) testSpec.getConfig().get("isCustomMessageSupported")); + } + if (testSpec.getConfig().containsKey("readOnly")) { + config.setReadOnly((Boolean) testSpec.getConfig().get("readOnly")); + } + if (testSpec.getConfig().containsKey("writeOnly")) { + config.setWriteOnly((Boolean) testSpec.getConfig().get("writeOnly")); + } } URI testCaseFileUri = URI.create("classpath:" + toForwardSlashPath(testSpec.getTestCase().getSpecification())); diff --git a/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java b/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java index bb131b57a..7ac837943 100644 --- a/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java +++ b/src/test/java/com/networknt/schema/ReadOnlyValidatorTest.java @@ -48,7 +48,7 @@ private JsonSchema getJsonSchema(Boolean write) { private SchemaValidatorsConfig createSchemaConfig(Boolean write) { SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.setWriteMode(write); + config.setReadOnly(write); return config; } diff --git a/src/test/resources/draft2020-12/issue798.json b/src/test/resources/draft2020-12/issue798.json new file mode 100644 index 000000000..5e2402beb --- /dev/null +++ b/src/test/resources/draft2020-12/issue798.json @@ -0,0 +1,46 @@ +[ + { + "description": "issue798", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "a": { "type": "string" }, + "b": { "type": "string", "readOnly": true }, + "c": { "type": "string", "writeOnly": true }, + "d": { "type": "string", "readOnly": true, "writeOnly": true } + } + }, + "tests": [ + { + "description": "default behavior", + "data": { "a": "a string" }, + "valid": true, + "config": { "readOnly": true, "writeOnly": true } + }, + { + "description": "readonly behavior", + "data": { "a": "a string", "b": "a string" }, + "valid": false, + "config": { "readOnly": true, "writeOnly": true }, + "validationMessages": [ "$.b: is a readonly field, it cannot be changed" ] + }, + { + "description": "write-only behavior", + "data": { "a": "a string", "c": "a string" }, + "valid": false, + "config": { "readOnly": true, "writeOnly": true }, + "validationMessages": [ "$.c: is a write-only field, it cannot appear in the data" ] + }, + { + "description": "both behavior", + "data": { "a": "a string", "d": "a string" }, + "valid": false, + "config": { "readOnly": true, "writeOnly": true }, + "validationMessages": [ + "$.d: is a readonly field, it cannot be changed", + "$.d: is a write-only field, it cannot appear in the data" + ] + } + ] + } +]