Skip to content

Commit cb248c3

Browse files
committed
changing ReadOnlyValidator to use boolean property instead of array.
Including the concept of read/write modes.
1 parent dcd6749 commit cb248c3

File tree

5 files changed

+172
-102
lines changed

5 files changed

+172
-102
lines changed

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

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,82 +16,34 @@
1616

1717
package com.networknt.schema;
1818

19-
import com.fasterxml.jackson.databind.JsonNode;
19+
import java.util.Set;
20+
2021
import org.slf4j.Logger;
2122
import org.slf4j.LoggerFactory;
2223

23-
import java.util.*;
24+
import com.fasterxml.jackson.databind.JsonNode;
2425

2526
public class ReadOnlyValidator extends BaseJsonValidator implements JsonValidator {
2627
private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
2728

28-
private List<String> fieldNames = new ArrayList<String>();
29+
private Boolean writeMode;
2930

30-
public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
31+
public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
32+
ValidationContext validationContext) {
3133
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.READ_ONLY, validationContext);
32-
if (schemaNode.isArray()) {
33-
int size = schemaNode.size();
34-
for (int i = 0; i < size; i++) {
35-
fieldNames.add(schemaNode.get(i).asText());
36-
}
37-
}
38-
34+
this.writeMode = validationContext.getConfig().isWriteMode();
35+
String mode = writeMode ? "write mode" : "read mode";
36+
logger.debug("Loaded ReadOnlyValidator for property {} as {}", parentSchema, mode);
3937
parseErrorCode(getValidatorType().getErrorCodeKey());
4038
}
4139

4240
public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
4341
debug(logger, node, rootNode, at);
44-
45-
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
46-
47-
for (String fieldName : fieldNames) {
48-
JsonNode propertyNode = node.get(fieldName);
49-
String datapath = "";
50-
if (at.equals("$")) {
51-
datapath = datapath + "#original." + fieldName;
52-
} else {
53-
datapath = datapath + "#original." + at.substring(2) + "." + fieldName;
54-
}
55-
JsonNode originalNode = getNode(datapath, rootNode);
56-
57-
boolean theSame = propertyNode != null && originalNode != null && propertyNode.equals(originalNode);
58-
if (!theSame) {
59-
errors.add(buildValidationMessage(at));
60-
}
61-
}
62-
63-
return Collections.unmodifiableSet(errors);
64-
}
65-
66-
private JsonNode getNode(String datapath, JsonNode data) {
67-
String path = getSubString(datapath,"$.");
68-
69-
String[] parts = path.split("\\.");
70-
JsonNode result = null;
71-
for (int i = 0; i < parts.length; i++) {
72-
if (parts[i].contains("[")) {
73-
int idx1 = parts[i].indexOf("[");
74-
int idx2 = parts[i].indexOf("]");
75-
String key = parts[i].substring(0, idx1).trim();
76-
int idx = Integer.parseInt(parts[i].substring(idx1 + 1, idx2).trim());
77-
result = data.get(key).get(idx);
78-
} else {
79-
result = data.get(parts[i]);
80-
}
81-
if (result == null) {
82-
break;
83-
}
84-
data = result;
85-
}
86-
return result;
87-
}
88-
89-
private String getSubString(String datapath, String keyword){
90-
String path = datapath;
91-
if (path.startsWith(keyword)) {
92-
path = path.substring(2);
42+
if (writeMode) {
43+
return Set.of(buildValidationMessage(at));
44+
} else {
45+
return Set.of();
9346
}
94-
return path;
9547
}
9648

97-
}
49+
}

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

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,20 @@
2727

2828
public class SchemaValidatorsConfig {
2929
/**
30-
* when validate type, if TYPE_LOOSE = true, will try to convert string to different types to match the type defined in
31-
* schema.
30+
* when validate type, if TYPE_LOOSE = true, will try to convert string to
31+
* different types to match the type defined in schema.
3232
*/
3333
private boolean typeLoose;
3434

3535
/**
36-
* When set to true, validator process is stop immediately when a very first validation error is discovered.
36+
* When set to true, validator process is stop immediately when a very first
37+
* validation error is discovered.
3738
*/
3839
private boolean failFast;
3940

4041
/**
41-
* When set to true, walker sets nodes that are missing or NullNode to the default value, if any, and mutate the input json.
42+
* When set to true, walker sets nodes that are missing or NullNode to the
43+
* default value, if any, and mutate the input json.
4244
*/
4345
private ApplyDefaultsStrategy applyDefaultsStrategy;
4446

@@ -48,7 +50,8 @@ public class SchemaValidatorsConfig {
4850
private boolean ecma262Validator;
4951

5052
/**
51-
* When set to true, use Java-specific semantics rather than native JavaScript semantics
53+
* When set to true, use Java-specific semantics rather than native JavaScript
54+
* semantics
5255
*/
5356
private boolean javaSemantics;
5457

@@ -58,38 +61,47 @@ public class SchemaValidatorsConfig {
5861
private boolean losslessNarrowing;
5962

6063
/**
61-
* When set to true, support for discriminators is enabled for validations of oneOf, anyOf and allOf as described
62-
* on <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">GitHub</a>.
64+
* When set to true, support for discriminators is enabled for validations of
65+
* oneOf, anyOf and allOf as described on <a href=
66+
* "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">GitHub</a>.
6367
*/
6468
private boolean openAPI3StyleDiscriminators = false;
6569

6670
/**
67-
* Map of public, normally internet accessible schema URLs to alternate locations; this allows for offline
68-
* validation of schemas that refer to public URLs. This is merged with any mappings the {@link JsonSchemaFactory}
69-
* may have been built with.
71+
* Map of public, normally internet accessible schema URLs to alternate
72+
* locations; this allows for offline validation of schemas that refer to public
73+
* URLs. This is merged with any mappings the {@link JsonSchemaFactory} may have
74+
* been built with.
7075
*/
7176
private Map<String, String> uriMappings = new HashMap<String, String>();
7277

7378
/**
74-
* When a field is set as nullable in the OpenAPI specification, the schema validator validates that it is nullable
75-
* however continues with validation against the nullable field
79+
* When a field is set as nullable in the OpenAPI specification, the schema
80+
* validator validates that it is nullable however continues with validation
81+
* against the nullable field
7682
* <p>
77-
* If handleNullableField is set to true && incoming field is nullable && value is field: null --> succeed
78-
* If handleNullableField is set to false && incoming field is nullable && value is field: null --> it is up to the type
79-
* validator using the SchemaValidator to handle it.
83+
* If handleNullableField is set to true && incoming field is nullable && value
84+
* is field: null --> succeed If handleNullableField is set to false && incoming
85+
* field is nullable && value is field: null --> it is up to the type validator
86+
* using the SchemaValidator to handle it.
8087
*/
8188
private boolean handleNullableField = true;
8289

8390
/**
84-
* When set to true resets the {@link CollectorContext} by calling {@link CollectorContext#reset()}.
91+
* When set to true resets the {@link CollectorContext} by calling
92+
* {@link CollectorContext#reset()}.
8593
*/
8694
private boolean resetCollectorContext = true;
8795

96+
/**
97+
* When set to true considers that schema is used to write data then ReadOnlyValidator is activated. Default true.
98+
*/
99+
private boolean writeMode = true;
100+
88101
// This is just a constant for listening to all Keywords.
89102
public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener";
90103

91-
private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap = new HashMap<String,
92-
List<JsonSchemaWalkListener>>();
104+
private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap = new HashMap<String, List<JsonSchemaWalkListener>>();
93105

94106
private final List<JsonSchemaWalkListener> propertyWalkListeners = new ArrayList<JsonSchemaWalkListener>();
95107

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

110122
/**
111-
* When enabled, {@link JsonValidator#validate(JsonNode, JsonNode, String)}
112-
* or {@link JsonValidator#validate(JsonNode)} doesn't return any {@link Set}&lt;{@link ValidationMessage}&gt;,
113-
* instead a {@link JsonSchemaException} is thrown as soon as a validation errors is discovered.
123+
* When enabled, {@link JsonValidator#validate(JsonNode, JsonNode, String)} or
124+
* {@link JsonValidator#validate(JsonNode)} doesn't return any
125+
* {@link Set}&lt;{@link ValidationMessage}&gt;, instead a
126+
* {@link JsonSchemaException} is thrown as soon as a validation errors is
127+
* discovered.
114128
*
115129
* @param failFast boolean
116130
*/
@@ -244,6 +258,7 @@ public void setLosslessNarrowing(boolean losslessNarrowing) {
244258

245259
/**
246260
* Indicates whether OpenAPI 3 style discriminators should be supported
261+
*
247262
* @return true in case discriminators are enabled
248263
* @since 1.0.51
249264
*/
@@ -252,26 +267,36 @@ public boolean isOpenAPI3StyleDiscriminators() {
252267
}
253268

254269
/**
255-
* When enabled, the validation of <code>anyOf</code> and <code>allOf</code> in polymorphism will respect
256-
* OpenAPI 3 style discriminators as described in the
257-
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">OpenAPI 3.0.3 spec</a>.
258-
* The presence of a discriminator configuration on the schema will lead to the following changes in the behavior:
270+
* When enabled, the validation of <code>anyOf</code> and <code>allOf</code> in
271+
* polymorphism will respect OpenAPI 3 style discriminators as described in the
272+
* <a href=
273+
* "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">OpenAPI
274+
* 3.0.3 spec</a>. The presence of a discriminator configuration on the schema
275+
* will lead to the following changes in the behavior:
259276
* <ul>
260-
* <li>for <code>oneOf</code> the spec is unfortunately very vague. Whether <code>oneOf</code> semantics should be
261-
* affected by discriminators or not is not even 100% clear within the members of the OAS steering committee. Therefore
262-
* <code>oneOf</code> at the moment ignores discriminators</li>
263-
* <li>for <code>anyOf</code> the validation will choose one of the candidate schemas for validation based on the
264-
* discriminator property value and will pass validation when this specific schema passes. This is in particular useful
265-
* when the payload could match multiple candidates in the <code>anyOf</code> list and could lead to ambiguity. Example:
266-
* type B has all mandatory properties of A and adds more mandatory ones. Whether the payload is an A or B is determined
267-
* via the discriminator property name. A payload indicating it is an instance of B then requires passing the validation
268-
* of B and passing the validation of A would not be sufficient anymore.</li>
269-
* <li>for <code>allOf</code> use cases with discriminators defined on the copied-in parent type, it is possible to
270-
* automatically validate against a subtype. Example: some schema specifies that there is a field of type A. A carries
271-
* a discriminator field and B inherits from A. Then B is automatically a candidate for validation as well and will be
272-
* chosen in case the discriminator property matches</li>
277+
* <li>for <code>oneOf</code> the spec is unfortunately very vague. Whether
278+
* <code>oneOf</code> semantics should be affected by discriminators or not is
279+
* not even 100% clear within the members of the OAS steering committee.
280+
* Therefore <code>oneOf</code> at the moment ignores discriminators</li>
281+
* <li>for <code>anyOf</code> the validation will choose one of the candidate
282+
* schemas for validation based on the discriminator property value and will
283+
* pass validation when this specific schema passes. This is in particular
284+
* useful when the payload could match multiple candidates in the
285+
* <code>anyOf</code> list and could lead to ambiguity. Example: type B has all
286+
* mandatory properties of A and adds more mandatory ones. Whether the payload
287+
* is an A or B is determined via the discriminator property name. A payload
288+
* indicating it is an instance of B then requires passing the validation of B
289+
* and passing the validation of A would not be sufficient anymore.</li>
290+
* <li>for <code>allOf</code> use cases with discriminators defined on the
291+
* copied-in parent type, it is possible to automatically validate against a
292+
* subtype. Example: some schema specifies that there is a field of type A. A
293+
* carries a discriminator field and B inherits from A. Then B is automatically
294+
* a candidate for validation as well and will be chosen in case the
295+
* discriminator property matches</li>
273296
* </ul>
274-
* @param openAPI3StyleDiscriminators whether or not discriminators should be used. Defaults to <code>false</code>
297+
*
298+
* @param openAPI3StyleDiscriminators whether or not discriminators should be
299+
* used. Defaults to <code>false</code>
275300
* @since 1.0.51
276301
*/
277302
public void setOpenAPI3StyleDiscriminators(boolean openAPI3StyleDiscriminators) {
@@ -293,4 +318,18 @@ public boolean isResetCollectorContext() {
293318
public void setResetCollectorContext(boolean resetCollectorContext) {
294319
this.resetCollectorContext = resetCollectorContext;
295320
}
296-
}
321+
322+
public boolean isWriteMode() {
323+
return writeMode;
324+
}
325+
326+
/**
327+
*
328+
* When set to true considers that schema is used to write data then ReadOnlyValidator is activated. Default true.
329+
*
330+
* @param writeMode
331+
*/
332+
public void setWriteMode(boolean writeMode) {
333+
this.writeMode = writeMode;
334+
}
335+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.networknt.schema;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.util.Set;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
import com.fasterxml.jackson.databind.ObjectMapper;
14+
import com.fasterxml.jackson.databind.node.ObjectNode;
15+
16+
class ReadOnlyValidatorTest {
17+
18+
@Test
19+
void givenConfigWriteFalseWhenReadOnlyTrueThenAllows() throws IOException {
20+
ObjectNode node = getJsonNode();
21+
Set<ValidationMessage> errors = loadJsonSchema(false).validate(node);
22+
assertTrue(errors.isEmpty());
23+
}
24+
25+
@Test
26+
void givenConfigWriteTrueWhenReadOnlyTrueThenDenies() throws IOException {
27+
ObjectNode node = getJsonNode();
28+
Set<ValidationMessage> errors = loadJsonSchema(true).validate(node);
29+
assertFalse(errors.isEmpty());
30+
assertEquals("$.firstName: is a readonly field, it cannot be changed",
31+
errors.stream().map(e -> e.getMessage()).toList().get(0));
32+
}
33+
34+
private JsonSchema loadJsonSchema(Boolean write) {
35+
JsonSchema schema = this.getJsonSchema(write);
36+
schema.initializeValidators();
37+
return schema;
38+
39+
}
40+
41+
private JsonSchema getJsonSchema(Boolean write) {
42+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
43+
SchemaValidatorsConfig schemaConfig = createSchemaConfig(write);
44+
InputStream schema = getClass().getClassLoader().getResourceAsStream("schema/read-only-schema.json");
45+
return factory.getSchema(schema, schemaConfig);
46+
}
47+
48+
private SchemaValidatorsConfig createSchemaConfig(Boolean write) {
49+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
50+
config.setWriteMode(write);
51+
return config;
52+
}
53+
54+
private ObjectNode getJsonNode() throws IOException {
55+
InputStream node = getClass().getClassLoader().getResourceAsStream("data/read-only-data.json");
56+
ObjectMapper mapper = new ObjectMapper();
57+
return (ObjectNode) mapper.readTree(node);
58+
}
59+
60+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"firstName": "George",
3+
"lastName": "Harrison"
4+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "Read Only Schema",
4+
"description": "Testing Read Only Schema Validation",
5+
"type": "object",
6+
"properties": {
7+
"firstName": {
8+
"type": "string",
9+
"readOnly": true
10+
},
11+
"lastName": {
12+
"type": "string"
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)