Skip to content

Commit 527fe6b

Browse files
aznan2Matti Hansson
and
Matti Hansson
authored
Add support for subschema references in getSchema(URI) (#619) (#625)
Co-authored-by: Matti Hansson <[email protected]>
1 parent 63e0ceb commit 527fe6b

File tree

5 files changed

+262
-15
lines changed

5 files changed

+262
-15
lines changed

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<modelVersion>4.0.0</modelVersion>
2121
<groupId>com.networknt</groupId>
2222
<artifactId>json-schema-validator</artifactId>
23-
<version>1.0.73</version>
23+
<version>1.0.74-SNAPSHOT</version>
2424
<packaging>bundle</packaging>
2525
<description>A json schema validator that supports draft v4, v6, v7, v2019-09 and v2020-12</description>
2626
<url>https://github.com/networknt/json-schema-validator</url>
@@ -170,7 +170,7 @@
170170
<plugin>
171171
<groupId>org.apache.felix</groupId>
172172
<artifactId>maven-bundle-plugin</artifactId>
173-
<version>4.2.1</version>
173+
<version>5.1.8</version>
174174
<extensions>true</extensions>
175175
<configuration>
176176
<instructions>

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

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,23 @@
1616

1717
package com.networknt.schema;
1818

19+
import com.fasterxml.jackson.databind.JsonNode;
20+
import com.fasterxml.jackson.databind.node.ObjectNode;
21+
import com.networknt.schema.ValidationContext.DiscriminatorContext;
22+
import com.networknt.schema.utils.StringUtils;
23+
import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
24+
import com.networknt.schema.walk.JsonSchemaWalker;
25+
import com.networknt.schema.walk.WalkListenerRunner;
26+
1927
import java.io.UnsupportedEncodingException;
2028
import java.net.URI;
29+
import java.net.URISyntaxException;
2130
import java.net.URLDecoder;
2231
import java.util.*;
2332
import java.util.Map.Entry;
2433
import java.util.regex.Matcher;
2534
import java.util.regex.Pattern;
2635

27-
import com.fasterxml.jackson.databind.JsonNode;
28-
import com.fasterxml.jackson.databind.node.ObjectNode;
29-
import com.networknt.schema.ValidationContext.DiscriminatorContext;
30-
import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
31-
import com.networknt.schema.walk.JsonSchemaWalker;
32-
import com.networknt.schema.walk.WalkListenerRunner;
33-
3436
/**
3537
* This is the core of json constraint implementation. It parses json constraint
3638
* file and generates JsonValidators. The class is thread safe, once it is
@@ -51,8 +53,7 @@ public class JsonSchema extends BaseJsonValidator {
5153
* This can be null. If it is null, then the creation of relative uris will fail. However, an absolute
5254
* 'id' would still be able to specify an absolute uri.
5355
*/
54-
private final URI currentUri;
55-
56+
private URI currentUri;
5657
private JsonValidator requiredValidator = null;
5758

5859
private JsonValidator unevaluatedPropertiesValidator = null;
@@ -79,7 +80,10 @@ private JsonSchema(ValidationContext validationContext, String schemaPath, URI c
7980
validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null);
8081
this.validationContext = validationContext;
8182
this.metaSchema = validationContext.getMetaSchema();
82-
this.currentUri = this.combineCurrentUriWithIds(currentUri, schemaNode);
83+
this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode);
84+
if (uriRefersToSubschema(currentUri, schemaPath)) {
85+
updateThisAsSubschema(currentUri);
86+
}
8387
if (validationContext.getConfig() != null) {
8488
keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap());
8589
if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
@@ -118,6 +122,35 @@ private boolean isUriFragmentWithNoContext(URI currentUri, String id) {
118122
return id.startsWith("#") && currentUri == null;
119123
}
120124

125+
private boolean uriRefersToSubschema(URI originalUri, String schemaPath) {
126+
return originalUri != null
127+
&& StringUtils.isNotBlank(originalUri.getRawFragment()) // Original currentUri parameter has a fragment, so it refers to a subschema
128+
&& (StringUtils.isBlank(schemaPath) || "#".equals(schemaPath)); // We aren't already in a subschema
129+
}
130+
131+
/**
132+
* Creates a new parent schema from the current state and updates this object to refer to the subschema instead.
133+
*/
134+
private void updateThisAsSubschema(URI originalUri) {
135+
String fragment = "#" + originalUri.getFragment();
136+
JsonNode fragmentSchemaNode = getRefSchemaNode(fragment);
137+
if (fragmentSchemaNode == null) {
138+
throw new JsonSchemaException("Fragment " + fragment + " cannot be resolved");
139+
}
140+
// We need to strip the fragment off of the new parent schema's currentUri, so that its constructor
141+
// won't also end up in this method and get stuck in an infinite recursive loop.
142+
URI currentUriWithoutFragment;
143+
try {
144+
currentUriWithoutFragment = new URI(currentUri.getScheme(), currentUri.getSchemeSpecificPart(), null);
145+
} catch (URISyntaxException ex) {
146+
throw new JsonSchemaException("Unable to create URI without fragment from " + currentUri + ": " + ex.getMessage());
147+
}
148+
this.parentSchema = new JsonSchema(validationContext, schemaPath, currentUriWithoutFragment, schemaNode, parentSchema);
149+
this.schemaPath = fragment;
150+
this.schemaNode = fragmentSchemaNode;
151+
this.currentUri = combineCurrentUriWithIds(currentUri, fragmentSchemaNode);
152+
}
153+
121154
public URI getCurrentUri() {
122155
return this.currentUri;
123156
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,9 @@ public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig co
375375

376376
JsonSchema jsonSchema;
377377
if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri)) {
378-
jsonSchema = new JsonSchema(
379-
new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config),
380-
mappedUri, schemaNode, true /* retrieved via id, resolving will not change anything */);
378+
jsonSchema = new JsonSchema(
379+
new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config),
380+
mappedUri, schemaNode, true /* retrieved via id, resolving will not change anything */);
381381
} else {
382382
final ValidationContext validationContext = createValidationContext(schemaNode);
383383
validationContext.setConfig(config);
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright (c) 2020 Network New Technologies Inc.
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 com.fasterxml.jackson.databind.JsonNode;
19+
import io.undertow.Undertow;
20+
import io.undertow.server.handlers.resource.FileResourceManager;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
24+
import java.io.File;
25+
import java.net.URI;
26+
27+
import static io.undertow.Handlers.resource;
28+
import static org.junit.jupiter.api.Assertions.*;
29+
30+
public class Issue619Test extends BaseJsonSchemaValidatorTest {
31+
32+
private JsonSchemaFactory factory;
33+
private JsonNode one;
34+
private JsonNode two;
35+
private JsonNode three;
36+
37+
@BeforeEach
38+
public void setup() throws Exception {
39+
factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
40+
one = getJsonNodeFromStringContent("1");
41+
two = getJsonNodeFromStringContent("2");
42+
three = getJsonNodeFromStringContent("3");
43+
}
44+
45+
@Test
46+
public void bundledSchemaLoadsAndValidatesCorrectly_Ref() {
47+
JsonSchema referencingRootSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json\" }");
48+
49+
assertTrue(referencingRootSchema.validate(one).isEmpty());
50+
assertTrue(referencingRootSchema.validate(two).isEmpty());
51+
assertFalse(referencingRootSchema.validate(three).isEmpty());
52+
}
53+
54+
@Test
55+
public void bundledSchemaLoadsAndValidatesCorrectly_Uri() throws Exception {
56+
JsonSchema rootSchema = factory.getSchema(new URI("resource:schema/issue619.json"));
57+
58+
assertTrue(rootSchema.validate(one).isEmpty());
59+
assertTrue(rootSchema.validate(two).isEmpty());
60+
assertFalse(rootSchema.validate(three).isEmpty());
61+
}
62+
63+
@Test
64+
public void uriWithEmptyFragment_Ref() {
65+
JsonSchema referencingRootSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#\" }");
66+
67+
assertTrue(referencingRootSchema.validate(one).isEmpty());
68+
assertTrue(referencingRootSchema.validate(two).isEmpty());
69+
assertFalse(referencingRootSchema.validate(three).isEmpty());
70+
}
71+
72+
@Test
73+
public void uriWithEmptyFragment_Uri() throws Exception {
74+
JsonSchema rootSchema = factory.getSchema(new URI("resource:schema/issue619.json#"));
75+
76+
assertTrue(rootSchema.validate(one).isEmpty());
77+
assertTrue(rootSchema.validate(two).isEmpty());
78+
assertFalse(rootSchema.validate(three).isEmpty());
79+
}
80+
81+
@Test
82+
public void uriThatPointsToTwoShouldOnlyValidateTwo_Ref() {
83+
JsonSchema referencingTwoSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#/definitions/two\" }");
84+
85+
assertFalse(referencingTwoSchema.validate(one).isEmpty());
86+
assertTrue(referencingTwoSchema.validate(two).isEmpty());
87+
assertFalse(referencingTwoSchema.validate(three).isEmpty());
88+
}
89+
90+
@Test
91+
public void uriThatPointsToOneShouldOnlyValidateOne_Uri() throws Exception {
92+
JsonSchema oneSchema = factory.getSchema(new URI("resource:schema/issue619.json#/definitions/one"));
93+
94+
assertTrue(oneSchema.validate(one).isEmpty());
95+
assertFalse(oneSchema.validate(two).isEmpty());
96+
assertFalse(oneSchema.validate(three).isEmpty());
97+
}
98+
99+
@Test
100+
public void uriThatPointsToNodeThatInTurnReferencesOneShouldOnlyValidateOne_Ref() {
101+
JsonSchema referencingTwoSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#/definitions/refToOne\" }");
102+
103+
assertTrue(referencingTwoSchema.validate(one).isEmpty());
104+
assertFalse(referencingTwoSchema.validate(two).isEmpty());
105+
assertFalse(referencingTwoSchema.validate(three).isEmpty());
106+
}
107+
108+
@Test
109+
public void uriThatPointsToNodeThatInTurnReferencesOneShouldOnlyValidateOne_Uri() throws Exception {
110+
JsonSchema oneSchema = factory.getSchema(new URI("resource:schema/issue619.json#/definitions/refToOne"));
111+
112+
assertTrue(oneSchema.validate(one).isEmpty());
113+
assertFalse(oneSchema.validate(two).isEmpty());
114+
assertFalse(oneSchema.validate(three).isEmpty());
115+
}
116+
117+
@Test
118+
public void uriThatPointsToSchemaWithIdThatHasDifferentUri_Ref() throws Exception {
119+
runLocalServer(() -> {
120+
JsonNode oneArray = getJsonNodeFromStringContent("[[1]]");
121+
JsonNode textArray = getJsonNodeFromStringContent("[[\"a\"]]");
122+
123+
JsonSchema schemaWithIdFromRef = factory.getSchema("{ \"$ref\": \"resource:draft4/refRemote.json#/3/schema\" }");
124+
assertTrue(schemaWithIdFromRef.validate(oneArray).isEmpty());
125+
assertFalse(schemaWithIdFromRef.validate(textArray).isEmpty());
126+
});
127+
}
128+
129+
@Test
130+
public void uriThatPointsToSchemaWithIdThatHasDifferentUri_Uri() throws Exception {
131+
runLocalServer(() -> {
132+
JsonNode oneArray = getJsonNodeFromStringContent("[[1]]");
133+
JsonNode textArray = getJsonNodeFromStringContent("[[\"a\"]]");
134+
135+
JsonSchema schemaWithIdFromUri = factory.getSchema(new URI("resource:draft4/refRemote.json#/3/schema"));
136+
assertTrue(schemaWithIdFromUri.validate(oneArray).isEmpty());
137+
assertFalse(schemaWithIdFromUri.validate(textArray).isEmpty());
138+
});
139+
}
140+
141+
private interface ThrowingRunnable {
142+
void run() throws Exception;
143+
}
144+
145+
private void runLocalServer(ThrowingRunnable actualTest) throws Exception {
146+
Undertow server = Undertow.builder()
147+
.addHttpListener(1234, "localhost")
148+
.setHandler(resource(new FileResourceManager(
149+
new File("./src/test/resources/remotes"), 100)))
150+
.build();
151+
try {
152+
server.start();
153+
154+
actualTest.run();
155+
156+
} finally {
157+
try {
158+
Thread.sleep(100);
159+
} catch (InterruptedException ignored) {
160+
Thread.currentThread().interrupt();
161+
}
162+
server.stop();
163+
}
164+
}
165+
166+
@Test
167+
public void uriThatPointsToSchemaThatDoesNotExistShouldFail_Ref() {
168+
JsonSchema referencingNonexistentSchema = factory.getSchema("{ \"$ref\": \"resource:data/schema-that-does-not-exist.json#/definitions/something\" }");
169+
170+
assertThrows(JsonSchemaException.class, () -> referencingNonexistentSchema.validate(one));
171+
}
172+
173+
@Test
174+
public void uriThatPointsToSchemaThatDoesNotExistShouldFail_Uri() {
175+
assertThrows(JsonSchemaException.class, () -> factory.getSchema(new URI("resource:data/schema-that-does-not-exist.json#/definitions/something")));
176+
}
177+
178+
@Test
179+
public void uriThatPointsToNodeThatDoesNotExistShouldFail_Ref() {
180+
JsonSchema referencingNonexistentSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#/definitions/node-that-does-not-exist\" }");
181+
182+
assertThrows(JsonSchemaException.class, () -> referencingNonexistentSchema.validate(one));
183+
}
184+
185+
@Test
186+
public void uriThatPointsToNodeThatDoesNotExistShouldFail_Uri() {
187+
assertThrows(JsonSchemaException.class, () -> factory.getSchema(new URI("resource:schema/issue619.json#/definitions/node-that-does-not-exist")));
188+
}
189+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"id": "resource:schema/issue619.json",
4+
"oneOf": [
5+
{
6+
"$ref": "#/definitions/one"
7+
},
8+
{
9+
"$ref": "#/definitions/two"
10+
}
11+
],
12+
"definitions": {
13+
"one": {
14+
"type": "integer",
15+
"enum": [1]
16+
},
17+
"two": {
18+
"type": "integer",
19+
"enum": [2]
20+
},
21+
"refToOne": {
22+
"$ref": "#/definitions/one"
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)