Skip to content

Commit a8bda4c

Browse files
authored
Support readOnly and writeOnly for required validation (#1186)
1 parent d7cb013 commit a8bda4c

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed

src/main/java/com/networknt/schema/RequiredValidator.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
5555
JsonNode propertyNode = node.get(fieldName);
5656

5757
if (propertyNode == null) {
58+
Boolean readOnly = this.validationContext.getConfig().getReadOnly();
59+
Boolean writeOnly = this.validationContext.getConfig().getWriteOnly();
60+
if (Boolean.TRUE.equals(readOnly)) {
61+
JsonNode readOnlyNode = getFieldKeyword(fieldName, "readOnly");
62+
if (readOnlyNode != null && readOnlyNode.booleanValue()) {
63+
continue;
64+
}
65+
} else if(Boolean.TRUE.equals(writeOnly)) {
66+
JsonNode writeOnlyNode = getFieldKeyword(fieldName, "writeOnly");
67+
if (writeOnlyNode != null && writeOnlyNode.booleanValue()) {
68+
continue;
69+
}
70+
}
5871
if (errors == null) {
5972
errors = new LinkedHashSet<>();
6073
}
@@ -72,4 +85,14 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
7285
return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors);
7386
}
7487

88+
protected JsonNode getFieldKeyword(String fieldName, String keyword) {
89+
JsonNode propertiesNode = this.parentSchema.getSchemaNode().get("properties");
90+
if (propertiesNode != null) {
91+
JsonNode fieldNode = propertiesNode.get(fieldName);
92+
if (fieldNode != null) {
93+
return fieldNode.get(keyword);
94+
}
95+
}
96+
return null;
97+
}
7598
}

src/main/java/com/networknt/schema/SchemaValidatorsConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,10 @@ public boolean isReadOnly() {
510510
return null != this.readOnly && this.readOnly;
511511
}
512512

513+
Boolean getReadOnly() {
514+
return this.readOnly;
515+
}
516+
513517
/**
514518
* Answers whether a keyword's validators may relax their analysis. The
515519
* default is to perform strict checking. One must explicitly allow a
@@ -548,6 +552,10 @@ public boolean isWriteOnly() {
548552
return null != this.writeOnly && this.writeOnly;
549553
}
550554

555+
Boolean getWriteOnly() {
556+
return this.writeOnly;
557+
}
558+
551559
public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) {
552560
this.applyDefaultsStrategy = applyDefaultsStrategy != null ? applyDefaultsStrategy
553561
: ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright (c) 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import com.networknt.schema.SpecVersion.VersionFlag;
26+
27+
/**
28+
* RequiredValidatorTest.
29+
*/
30+
class RequiredValidatorTest {
31+
/**
32+
* Tests that when validating requests with required read only properties they
33+
* are ignored.
34+
*/
35+
@Test
36+
void validateRequestRequiredReadOnlyShouldBeIgnored() {
37+
String schemaData = "{\r\n"
38+
+ " \"type\": \"object\",\r\n"
39+
+ " \"properties\": {\r\n"
40+
+ " \"amount\": {\r\n"
41+
+ " \"type\": \"number\",\r\n"
42+
+ " \"writeOnly\": true\r\n"
43+
+ " },\r\n"
44+
+ " \"description\": {\r\n"
45+
+ " \"type\": \"string\"\r\n"
46+
+ " },\r\n"
47+
+ " \"name\": {\r\n"
48+
+ " \"type\": \"string\",\r\n"
49+
+ " \"readOnly\": true\r\n"
50+
+ " }\r\n"
51+
+ " },\r\n"
52+
+ " \"required\": [\r\n"
53+
+ " \"amount\",\r\n"
54+
+ " \"description\",\r\n"
55+
+ " \"name\"\r\n"
56+
+ " ]\r\n"
57+
+ "}";
58+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
59+
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().readOnly(true).build();
60+
JsonSchema schema = factory.getSchema(schemaData, config);
61+
String inputData = "{\r\n"
62+
+ " \"foo\":\"hello\",\r\n"
63+
+ " \"bar\":\"world\"\r\n"
64+
+ "}";
65+
List<ValidationMessage> messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON));
66+
assertEquals(messages.size(), 2);
67+
ValidationMessage message = messages.get(0);
68+
assertEquals("/required", message.getEvaluationPath().toString());
69+
assertEquals("amount", message.getProperty());
70+
message = messages.get(1);
71+
assertEquals("/required", message.getEvaluationPath().toString());
72+
assertEquals("description", message.getProperty());
73+
}
74+
75+
/**
76+
* Tests that when validating responses with required write only properties they
77+
* are ignored.
78+
*/
79+
@Test
80+
void validateResponseRequiredWriteOnlyShouldBeIgnored() {
81+
String schemaData = "{\r\n"
82+
+ " \"type\": \"object\",\r\n"
83+
+ " \"properties\": {\r\n"
84+
+ " \"amount\": {\r\n"
85+
+ " \"type\": \"number\",\r\n"
86+
+ " \"writeOnly\": true\r\n"
87+
+ " },\r\n"
88+
+ " \"description\": {\r\n"
89+
+ " \"type\": \"string\"\r\n"
90+
+ " },\r\n"
91+
+ " \"name\": {\r\n"
92+
+ " \"type\": \"string\",\r\n"
93+
+ " \"readOnly\": true\r\n"
94+
+ " }\r\n"
95+
+ " },\r\n"
96+
+ " \"required\": [\r\n"
97+
+ " \"amount\",\r\n"
98+
+ " \"description\",\r\n"
99+
+ " \"name\"\r\n"
100+
+ " ]\r\n"
101+
+ "}";
102+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
103+
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().writeOnly(true).build();
104+
JsonSchema schema = factory.getSchema(schemaData, config);
105+
String inputData = "{\r\n"
106+
+ " \"foo\":\"hello\",\r\n"
107+
+ " \"bar\":\"world\"\r\n"
108+
+ "}";
109+
List<ValidationMessage> messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON));
110+
assertEquals(messages.size(), 2);
111+
ValidationMessage message = messages.get(0);
112+
assertEquals("/required", message.getEvaluationPath().toString());
113+
assertEquals("description", message.getProperty());
114+
message = messages.get(1);
115+
assertEquals("/required", message.getEvaluationPath().toString());
116+
assertEquals("name", message.getProperty());
117+
}
118+
119+
/**
120+
* Tests that when validating requests with required read only properties they
121+
* are ignored.
122+
*/
123+
@Test
124+
void validateRequestRequired() {
125+
String schemaData = "{\r\n"
126+
+ " \"type\": \"object\",\r\n"
127+
+ " \"properties\": {\r\n"
128+
+ " \"amount\": {\r\n"
129+
+ " \"type\": \"number\",\r\n"
130+
+ " \"writeOnly\": true\r\n"
131+
+ " },\r\n"
132+
+ " \"description\": {\r\n"
133+
+ " \"type\": \"string\"\r\n"
134+
+ " },\r\n"
135+
+ " \"name\": {\r\n"
136+
+ " \"type\": \"string\",\r\n"
137+
+ " \"readOnly\": true\r\n"
138+
+ " }\r\n"
139+
+ " },\r\n"
140+
+ " \"required\": [\r\n"
141+
+ " \"amount\",\r\n"
142+
+ " \"description\",\r\n"
143+
+ " \"name\"\r\n"
144+
+ " ]\r\n"
145+
+ "}";
146+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
147+
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().readOnly(true).build();
148+
JsonSchema schema = factory.getSchema(schemaData, config);
149+
String inputData = "{\r\n"
150+
+ " \"amount\":10,\r\n"
151+
+ " \"description\":\"world\"\r\n"
152+
+ "}";
153+
List<ValidationMessage> messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON));
154+
assertEquals(messages.size(), 0);
155+
}
156+
157+
/**
158+
* Tests that when validating response with required write only properties they
159+
* are ignored.
160+
*/
161+
@Test
162+
void validateResponseRequired() {
163+
String schemaData = "{\r\n"
164+
+ " \"type\": \"object\",\r\n"
165+
+ " \"properties\": {\r\n"
166+
+ " \"amount\": {\r\n"
167+
+ " \"type\": \"number\",\r\n"
168+
+ " \"writeOnly\": true\r\n"
169+
+ " },\r\n"
170+
+ " \"description\": {\r\n"
171+
+ " \"type\": \"string\"\r\n"
172+
+ " },\r\n"
173+
+ " \"name\": {\r\n"
174+
+ " \"type\": \"string\",\r\n"
175+
+ " \"readOnly\": true\r\n"
176+
+ " }\r\n"
177+
+ " },\r\n"
178+
+ " \"required\": [\r\n"
179+
+ " \"amount\",\r\n"
180+
+ " \"description\",\r\n"
181+
+ " \"name\"\r\n"
182+
+ " ]\r\n"
183+
+ "}";
184+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
185+
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().writeOnly(true).build();
186+
JsonSchema schema = factory.getSchema(schemaData, config);
187+
String inputData = "{\r\n"
188+
+ " \"description\":\"world\",\r\n"
189+
+ " \"name\":\"hello\"\r\n"
190+
+ "}";
191+
List<ValidationMessage> messages = new ArrayList<>(schema.validate(inputData, InputFormat.JSON));
192+
assertEquals(messages.size(), 0);
193+
}
194+
}

0 commit comments

Comments
 (0)