Skip to content

Commit 9b73d10

Browse files
authored
Support schema resource (#922)
* Support schema resources * Refactor * Refactor * Refactor * Support uri change in id * Fix schema resource parent and evaluation path * Fix * Fix remote ref paths * Fix * Fix schema location * Support anchors * Refactor * Refactor * Refactor * Refactor shift subschema loading to factory * Fix ref * Refactor ref * Refactor discriminator * Refactor * Refactor validation context * Load validators in constructor * Schema location * Refactor ref validator * Fix enum
1 parent 5a94df7 commit 9b73d10

36 files changed

+1309
-362
lines changed

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,23 @@ public static String resolve(String parent, String iri) {
105105
} else {
106106
scheme = scheme + 3;
107107
}
108-
int slash = parent.lastIndexOf('/');
109-
if (slash != -1 && slash > scheme) {
110-
base = parent.substring(0, slash);
108+
base = parent(base, scheme);
109+
while (iri.startsWith("../")) {
110+
base = parent(base, scheme);
111+
iri = iri.substring(3);
111112
}
112113
return base + "/" + iri;
113114
}
114115
}
115116
}
117+
118+
protected static String parent(String iri, int scheme) {
119+
int slash = iri.lastIndexOf('/');
120+
if (slash != -1 && slash > scheme) {
121+
return iri.substring(0, slash);
122+
}
123+
return iri;
124+
}
116125

117126
/**
118127
* Returns the scheme and authority components of the IRI.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
6969
final ObjectNode allOfEntry = (ObjectNode) arrayElements.next();
7070
final JsonNode $ref = allOfEntry.get("$ref");
7171
if (null != $ref) {
72-
final ValidationContext.DiscriminatorContext currentDiscriminatorContext = this.validationContext
72+
final DiscriminatorContext currentDiscriminatorContext = executionContext
7373
.getCurrentDiscriminatorContext();
7474
if (null != currentDiscriminatorContext) {
7575
final ObjectNode discriminator = currentDiscriminatorContext

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class AnyOfValidator extends BaseJsonValidator {
3030
private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation";
3131

3232
private final List<JsonSchema> schemas = new ArrayList<>();
33-
private final ValidationContext.DiscriminatorContext discriminatorContext;
33+
private final DiscriminatorContext discriminatorContext;
3434

3535
public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
3636
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext);
@@ -42,7 +42,7 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath
4242
}
4343

4444
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
45-
this.discriminatorContext = new ValidationContext.DiscriminatorContext();
45+
this.discriminatorContext = new DiscriminatorContext();
4646
} else {
4747
this.discriminatorContext = null;
4848
}
@@ -57,7 +57,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
5757
ValidatorState state = executionContext.getValidatorState();
5858

5959
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
60-
this.validationContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation);
60+
executionContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation);
6161
}
6262

6363
boolean initialHasMatchedNode = state.hasMatchedNode();
@@ -148,7 +148,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
148148
}
149149
} finally {
150150
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
151-
this.validationContext.leaveDiscriminatorContextImmediately(instanceLocation);
151+
executionContext.leaveDiscriminatorContextImmediately(instanceLocation);
152152
}
153153

154154
Scope parentScope = collectorContext.exitDynamicScope();

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
2020
import com.fasterxml.jackson.databind.node.ObjectNode;
21-
import com.networknt.schema.ValidationContext.DiscriminatorContext;
2221
import com.networknt.schema.i18n.DefaultMessageSource;
2322

2423
import org.slf4j.Logger;
@@ -69,6 +68,20 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP
6968
: PathType.DEFAULT;
7069
}
7170

71+
/**
72+
* Copy constructor.
73+
*
74+
* @param copy to copy from
75+
*/
76+
protected BaseJsonValidator(BaseJsonValidator copy) {
77+
super(copy);
78+
this.suppressSubSchemaRetrieval = copy.suppressSubSchemaRetrieval;
79+
this.applyDefaultsStrategy = copy.applyDefaultsStrategy;
80+
this.pathType = copy.pathType;
81+
this.schemaNode = copy.schemaNode;
82+
this.validationContext = copy.validationContext;
83+
}
84+
7285
private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) {
7386
final JsonNode node = schemaNode.get("id");
7487

@@ -112,7 +125,7 @@ protected static void debug(Logger logger, JsonNode node, JsonNode rootNode, Jso
112125
* @param discriminatorPropertyValue the value of the <code>discriminator/propertyName</code> field
113126
* @param jsonSchema the {@link JsonSchema} to check
114127
*/
115-
protected static void checkDiscriminatorMatch(final ValidationContext.DiscriminatorContext currentDiscriminatorContext,
128+
protected static void checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext,
116129
final ObjectNode discriminator,
117130
final String discriminatorPropertyValue,
118131
final JsonSchema jsonSchema) {
@@ -249,6 +262,13 @@ public JsonSchema getParentSchema() {
249262
return this.parentSchema;
250263
}
251264

265+
public JsonSchema getEvaluationParentSchema() {
266+
if (this.evaluationParentSchema != null) {
267+
return this.evaluationParentSchema;
268+
}
269+
return getParentSchema();
270+
}
271+
252272
protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) {
253273
return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext);
254274
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2023 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 java.util.function.Supplier;
19+
20+
/**
21+
* Supplier that caches the output.
22+
*
23+
* @param <T> the type cached
24+
*/
25+
public class CachedSupplier<T> implements Supplier<T> {
26+
private final Supplier<T> delegate;
27+
private T cache = null;
28+
29+
public CachedSupplier(Supplier<T> delegate) {
30+
this.delegate = delegate;
31+
}
32+
33+
@Override
34+
public T get() {
35+
if (cache == null) {
36+
cache = delegate.get();
37+
}
38+
return cache;
39+
}
40+
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.networknt.schema;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import com.fasterxml.jackson.databind.node.ObjectNode;
7+
8+
public class DiscriminatorContext {
9+
private final Map<String, ObjectNode> discriminators = new HashMap<>();
10+
11+
private boolean discriminatorMatchFound = false;
12+
13+
public void registerDiscriminator(final SchemaLocation schemaLocation, final ObjectNode discriminator) {
14+
this.discriminators.put("#" + schemaLocation.getFragment().toString(), discriminator);
15+
}
16+
17+
public ObjectNode getDiscriminatorForPath(final SchemaLocation schemaLocation) {
18+
return this.discriminators.get("#" + schemaLocation.getFragment().toString());
19+
}
20+
21+
public ObjectNode getDiscriminatorForPath(final String schemaLocation) {
22+
return this.discriminators.get(schemaLocation);
23+
}
24+
25+
public void markMatch() {
26+
this.discriminatorMatchFound = true;
27+
}
28+
29+
public boolean isDiscriminatorMatchFound() {
30+
return this.discriminatorMatchFound;
31+
}
32+
33+
/**
34+
* Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure
35+
*
36+
* @return true in case there are discriminator candidates
37+
*/
38+
public boolean isActive() {
39+
return !this.discriminators.isEmpty();
40+
}
41+
}

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

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package com.networknt.schema;
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
20+
import com.fasterxml.jackson.databind.node.ArrayNode;
2021
import com.fasterxml.jackson.databind.node.DecimalNode;
2122
import com.fasterxml.jackson.databind.node.NullNode;
2223
import org.slf4j.Logger;
2324
import org.slf4j.LoggerFactory;
2425

26+
import java.math.BigDecimal;
2527
import java.util.Collections;
2628
import java.util.HashSet;
2729
import java.util.Set;
@@ -45,7 +47,10 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
4547
for (JsonNode n : schemaNode) {
4648
if (n.isNumber()) {
4749
// convert to DecimalNode for number comparison
48-
nodes.add(DecimalNode.valueOf(n.decimalValue()));
50+
nodes.add(processNumberNode(n));
51+
} else if (n.isArray()) {
52+
ArrayNode a = processArrayNode((ArrayNode) n);
53+
nodes.add(a);
4954
} else {
5055
nodes.add(n);
5156
}
@@ -65,7 +70,6 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
6570
sb.append("null");
6671
}
6772
}
68-
//
6973
sb.append(']');
7074

7175
error = sb.toString();
@@ -78,7 +82,11 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
7882
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
7983
debug(logger, node, rootNode, instanceLocation);
8084

81-
if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue());
85+
if (node.isNumber()) {
86+
node = processNumberNode(node);
87+
} else if (node.isArray()) {
88+
node = processArrayNode((ArrayNode) node);
89+
}
8290
if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) {
8391
return Collections.singleton(message().instanceLocation(instanceLocation)
8492
.locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build());
@@ -105,4 +113,50 @@ private boolean isTypeLooseContainsInEnum(JsonNode node) {
105113
return false;
106114
}
107115

116+
/**
117+
* Processes the number and ensures trailing zeros are stripped.
118+
*
119+
* @param n the node
120+
* @return the node
121+
*/
122+
protected JsonNode processNumberNode(JsonNode n) {
123+
return DecimalNode.valueOf(new BigDecimal(n.decimalValue().toPlainString()));
124+
}
125+
126+
/**
127+
* Processes the array and ensures that numbers within have trailing zeroes stripped.
128+
*
129+
* @param node the node
130+
* @return the node
131+
*/
132+
protected ArrayNode processArrayNode(ArrayNode node) {
133+
if (!hasNumber(node)) {
134+
return node;
135+
}
136+
ArrayNode a = (ArrayNode) node.deepCopy();
137+
for (int x = 0; x < a.size(); x++) {
138+
JsonNode v = a.get(x);
139+
if (v.isNumber()) {
140+
v = processNumberNode(v);
141+
a.set(x, v);
142+
}
143+
}
144+
return a;
145+
}
146+
147+
/**
148+
* Determines if the array node contains a number.
149+
*
150+
* @param node the node
151+
* @return the node
152+
*/
153+
protected boolean hasNumber(ArrayNode node) {
154+
for (int x = 0; x < node.size(); x++) {
155+
JsonNode v = node.get(x);
156+
if (v.isNumber()) {
157+
return true;
158+
}
159+
}
160+
return false;
161+
}
108162
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616

1717
package com.networknt.schema;
1818

19+
import java.util.Stack;
20+
1921
/**
2022
* Stores the execution context for the validation run.
2123
*/
2224
public class ExecutionContext {
2325
private ExecutionConfig executionConfig;
2426
private CollectorContext collectorContext;
2527
private ValidatorState validatorState = null;
28+
private Stack<DiscriminatorContext> discriminatorContexts = new Stack<>();
2629

2730
/**
2831
* Creates an execution context.
@@ -113,4 +116,19 @@ public ValidatorState getValidatorState() {
113116
public void setValidatorState(ValidatorState validatorState) {
114117
this.validatorState = validatorState;
115118
}
119+
120+
public DiscriminatorContext getCurrentDiscriminatorContext() {
121+
if (!this.discriminatorContexts.empty()) {
122+
return this.discriminatorContexts.peek();
123+
}
124+
return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf
125+
}
126+
127+
public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") JsonNodePath instanceLocation) {
128+
this.discriminatorContexts.push(ctx);
129+
}
130+
131+
public void leaveDiscriminatorContextImmediately(@SuppressWarnings("unused") JsonNodePath instanceLocation) {
132+
this.discriminatorContexts.pop();
133+
}
116134
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ public String readId(JsonNode schemaNode) {
226226
return readText(schemaNode, this.idKeyword);
227227
}
228228

229+
public String readAnchor(JsonNode schemaNode) {
230+
boolean supportsAnchor = this.keywords.containsKey("$anchor");
231+
if (supportsAnchor) {
232+
return readText(schemaNode, "$anchor");
233+
}
234+
return null;
235+
}
236+
229237
public JsonNode getNodeByFragmentRef(String ref, JsonNode node) {
230238
boolean supportsAnchor = this.keywords.containsKey("$anchor");
231239
String refName = supportsAnchor ? ref.substring(1) : ref;

0 commit comments

Comments
 (0)