From dabc409813b1de0774016aaa853edde77ff4b667 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:12:47 +0800 Subject: [PATCH] Support readOnly and writeOnly for required validation --- .../networknt/schema/RequiredValidator.java | 23 +++ .../schema/SchemaValidatorsConfig.java | 8 + .../schema/RequiredValidatorTest.java | 194 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 src/test/java/com/networknt/schema/RequiredValidatorTest.java diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index b0bf63b6..7084b6b9 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -55,6 +55,19 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode == null) { + Boolean readOnly = this.validationContext.getConfig().getReadOnly(); + Boolean writeOnly = this.validationContext.getConfig().getWriteOnly(); + if (Boolean.TRUE.equals(readOnly)) { + JsonNode readOnlyNode = getFieldKeyword(fieldName, "readOnly"); + if (readOnlyNode != null && readOnlyNode.booleanValue()) { + continue; + } + } else if(Boolean.TRUE.equals(writeOnly)) { + JsonNode writeOnlyNode = getFieldKeyword(fieldName, "writeOnly"); + if (writeOnlyNode != null && writeOnlyNode.booleanValue()) { + continue; + } + } if (errors == null) { errors = new LinkedHashSet<>(); } @@ -72,4 +85,14 @@ public Set validate(ExecutionContext executionContext, JsonNo return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } + protected JsonNode getFieldKeyword(String fieldName, String keyword) { + JsonNode propertiesNode = this.parentSchema.getSchemaNode().get("properties"); + if (propertiesNode != null) { + JsonNode fieldNode = propertiesNode.get(fieldName); + if (fieldNode != null) { + return fieldNode.get(keyword); + } + } + return null; + } } diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index beb25d7d..a08f7abe 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -510,6 +510,10 @@ public boolean isReadOnly() { return null != this.readOnly && this.readOnly; } + Boolean getReadOnly() { + return this.readOnly; + } + /** * Answers whether a keyword's validators may relax their analysis. The * default is to perform strict checking. One must explicitly allow a @@ -548,6 +552,10 @@ public boolean isWriteOnly() { return null != this.writeOnly && this.writeOnly; } + Boolean getWriteOnly() { + return this.writeOnly; + } + public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) { this.applyDefaultsStrategy = applyDefaultsStrategy != null ? applyDefaultsStrategy : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; diff --git a/src/test/java/com/networknt/schema/RequiredValidatorTest.java b/src/test/java/com/networknt/schema/RequiredValidatorTest.java new file mode 100644 index 00000000..599b8783 --- /dev/null +++ b/src/test/java/com/networknt/schema/RequiredValidatorTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2025 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 java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * RequiredValidatorTest. + */ +class RequiredValidatorTest { + /** + * Tests that when validating requests with required read only properties they + * are ignored. + */ + @Test + void validateRequestRequiredReadOnlyShouldBeIgnored() { + String schemaData = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"amount\": {\r\n" + + " \"type\": \"number\",\r\n" + + " \"writeOnly\": true\r\n" + + " },\r\n" + + " \"description\": {\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"name\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"readOnly\": true\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"amount\",\r\n" + + " \"description\",\r\n" + + " \"name\"\r\n" + + " ]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().readOnly(true).build(); + JsonSchema schema = factory.getSchema(schemaData, config); + String inputData = "{\r\n" + + " \"foo\":\"hello\",\r\n" + + " \"bar\":\"world\"\r\n" + + "}"; + List messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON)); + assertEquals(messages.size(), 2); + ValidationMessage message = messages.get(0); + assertEquals("/required", message.getEvaluationPath().toString()); + assertEquals("amount", message.getProperty()); + message = messages.get(1); + assertEquals("/required", message.getEvaluationPath().toString()); + assertEquals("description", message.getProperty()); + } + + /** + * Tests that when validating responses with required write only properties they + * are ignored. + */ + @Test + void validateResponseRequiredWriteOnlyShouldBeIgnored() { + String schemaData = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"amount\": {\r\n" + + " \"type\": \"number\",\r\n" + + " \"writeOnly\": true\r\n" + + " },\r\n" + + " \"description\": {\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"name\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"readOnly\": true\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"amount\",\r\n" + + " \"description\",\r\n" + + " \"name\"\r\n" + + " ]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().writeOnly(true).build(); + JsonSchema schema = factory.getSchema(schemaData, config); + String inputData = "{\r\n" + + " \"foo\":\"hello\",\r\n" + + " \"bar\":\"world\"\r\n" + + "}"; + List messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON)); + assertEquals(messages.size(), 2); + ValidationMessage message = messages.get(0); + assertEquals("/required", message.getEvaluationPath().toString()); + assertEquals("description", message.getProperty()); + message = messages.get(1); + assertEquals("/required", message.getEvaluationPath().toString()); + assertEquals("name", message.getProperty()); + } + + /** + * Tests that when validating requests with required read only properties they + * are ignored. + */ + @Test + void validateRequestRequired() { + String schemaData = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"amount\": {\r\n" + + " \"type\": \"number\",\r\n" + + " \"writeOnly\": true\r\n" + + " },\r\n" + + " \"description\": {\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"name\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"readOnly\": true\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"amount\",\r\n" + + " \"description\",\r\n" + + " \"name\"\r\n" + + " ]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().readOnly(true).build(); + JsonSchema schema = factory.getSchema(schemaData, config); + String inputData = "{\r\n" + + " \"amount\":10,\r\n" + + " \"description\":\"world\"\r\n" + + "}"; + List messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON)); + assertEquals(messages.size(), 0); + } + + /** + * Tests that when validating response with required write only properties they + * are ignored. + */ + @Test + void validateResponseRequired() { + String schemaData = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"amount\": {\r\n" + + " \"type\": \"number\",\r\n" + + " \"writeOnly\": true\r\n" + + " },\r\n" + + " \"description\": {\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"name\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"readOnly\": true\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"amount\",\r\n" + + " \"description\",\r\n" + + " \"name\"\r\n" + + " ]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().writeOnly(true).build(); + JsonSchema schema = factory.getSchema(schemaData, config); + String inputData = "{\r\n" + + " \"description\":\"world\",\r\n" + + " \"name\":\"hello\"\r\n" + + "}"; + List messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON)); + assertEquals(messages.size(), 0); + } +}