Skip to content

Commit

Permalink
Add support for subschema references in getSchema(URI) (#619) (#625)
Browse files Browse the repository at this point in the history
Co-authored-by: Matti Hansson <matti.hansson@lm.se>
  • Loading branch information
aznan2 and Matti Hansson authored Dec 2, 2022
1 parent 63e0ceb commit 527fe6b
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 15 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.0.73</version>
<version>1.0.74-SNAPSHOT</version>
<packaging>bundle</packaging>
<description>A json schema validator that supports draft v4, v6, v7, v2019-09 and v2020-12</description>
<url>https://github.com/networknt/json-schema-validator</url>
Expand Down Expand Up @@ -170,7 +170,7 @@
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>4.2.1</version>
<version>5.1.8</version>
<extensions>true</extensions>
<configuration>
<instructions>
Expand Down
53 changes: 43 additions & 10 deletions src/main/java/com/networknt/schema/JsonSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,23 @@

package com.networknt.schema;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.ValidationContext.DiscriminatorContext;
import com.networknt.schema.utils.StringUtils;
import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
import com.networknt.schema.walk.JsonSchemaWalker;
import com.networknt.schema.walk.WalkListenerRunner;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.ValidationContext.DiscriminatorContext;
import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
import com.networknt.schema.walk.JsonSchemaWalker;
import com.networknt.schema.walk.WalkListenerRunner;

/**
* This is the core of json constraint implementation. It parses json constraint
* file and generates JsonValidators. The class is thread safe, once it is
Expand All @@ -51,8 +53,7 @@ public class JsonSchema extends BaseJsonValidator {
* This can be null. If it is null, then the creation of relative uris will fail. However, an absolute
* 'id' would still be able to specify an absolute uri.
*/
private final URI currentUri;

private URI currentUri;
private JsonValidator requiredValidator = null;

private JsonValidator unevaluatedPropertiesValidator = null;
Expand All @@ -79,7 +80,10 @@ private JsonSchema(ValidationContext validationContext, String schemaPath, URI c
validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null);
this.validationContext = validationContext;
this.metaSchema = validationContext.getMetaSchema();
this.currentUri = this.combineCurrentUriWithIds(currentUri, schemaNode);
this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode);
if (uriRefersToSubschema(currentUri, schemaPath)) {
updateThisAsSubschema(currentUri);
}
if (validationContext.getConfig() != null) {
keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap());
if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
Expand Down Expand Up @@ -118,6 +122,35 @@ private boolean isUriFragmentWithNoContext(URI currentUri, String id) {
return id.startsWith("#") && currentUri == null;
}

private boolean uriRefersToSubschema(URI originalUri, String schemaPath) {
return originalUri != null
&& StringUtils.isNotBlank(originalUri.getRawFragment()) // Original currentUri parameter has a fragment, so it refers to a subschema
&& (StringUtils.isBlank(schemaPath) || "#".equals(schemaPath)); // We aren't already in a subschema
}

/**
* Creates a new parent schema from the current state and updates this object to refer to the subschema instead.
*/
private void updateThisAsSubschema(URI originalUri) {
String fragment = "#" + originalUri.getFragment();
JsonNode fragmentSchemaNode = getRefSchemaNode(fragment);
if (fragmentSchemaNode == null) {
throw new JsonSchemaException("Fragment " + fragment + " cannot be resolved");
}
// We need to strip the fragment off of the new parent schema's currentUri, so that its constructor
// won't also end up in this method and get stuck in an infinite recursive loop.
URI currentUriWithoutFragment;
try {
currentUriWithoutFragment = new URI(currentUri.getScheme(), currentUri.getSchemeSpecificPart(), null);
} catch (URISyntaxException ex) {
throw new JsonSchemaException("Unable to create URI without fragment from " + currentUri + ": " + ex.getMessage());
}
this.parentSchema = new JsonSchema(validationContext, schemaPath, currentUriWithoutFragment, schemaNode, parentSchema);
this.schemaPath = fragment;
this.schemaNode = fragmentSchemaNode;
this.currentUri = combineCurrentUriWithIds(currentUri, fragmentSchemaNode);
}

public URI getCurrentUri() {
return this.currentUri;
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/networknt/schema/JsonSchemaFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,9 @@ public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig co

JsonSchema jsonSchema;
if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri)) {
jsonSchema = new JsonSchema(
new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config),
mappedUri, schemaNode, true /* retrieved via id, resolving will not change anything */);
jsonSchema = new JsonSchema(
new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config),
mappedUri, schemaNode, true /* retrieved via id, resolving will not change anything */);
} else {
final ValidationContext validationContext = createValidationContext(schemaNode);
validationContext.setConfig(config);
Expand Down
189 changes: 189 additions & 0 deletions src/test/java/com/networknt/schema/Issue619Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2020 Network New Technologies Inc.
*
* 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 com.fasterxml.jackson.databind.JsonNode;
import io.undertow.Undertow;
import io.undertow.server.handlers.resource.FileResourceManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.net.URI;

import static io.undertow.Handlers.resource;
import static org.junit.jupiter.api.Assertions.*;

public class Issue619Test extends BaseJsonSchemaValidatorTest {

private JsonSchemaFactory factory;
private JsonNode one;
private JsonNode two;
private JsonNode three;

@BeforeEach
public void setup() throws Exception {
factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
one = getJsonNodeFromStringContent("1");
two = getJsonNodeFromStringContent("2");
three = getJsonNodeFromStringContent("3");
}

@Test
public void bundledSchemaLoadsAndValidatesCorrectly_Ref() {
JsonSchema referencingRootSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json\" }");

assertTrue(referencingRootSchema.validate(one).isEmpty());
assertTrue(referencingRootSchema.validate(two).isEmpty());
assertFalse(referencingRootSchema.validate(three).isEmpty());
}

@Test
public void bundledSchemaLoadsAndValidatesCorrectly_Uri() throws Exception {
JsonSchema rootSchema = factory.getSchema(new URI("resource:schema/issue619.json"));

assertTrue(rootSchema.validate(one).isEmpty());
assertTrue(rootSchema.validate(two).isEmpty());
assertFalse(rootSchema.validate(three).isEmpty());
}

@Test
public void uriWithEmptyFragment_Ref() {
JsonSchema referencingRootSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#\" }");

assertTrue(referencingRootSchema.validate(one).isEmpty());
assertTrue(referencingRootSchema.validate(two).isEmpty());
assertFalse(referencingRootSchema.validate(three).isEmpty());
}

@Test
public void uriWithEmptyFragment_Uri() throws Exception {
JsonSchema rootSchema = factory.getSchema(new URI("resource:schema/issue619.json#"));

assertTrue(rootSchema.validate(one).isEmpty());
assertTrue(rootSchema.validate(two).isEmpty());
assertFalse(rootSchema.validate(three).isEmpty());
}

@Test
public void uriThatPointsToTwoShouldOnlyValidateTwo_Ref() {
JsonSchema referencingTwoSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#/definitions/two\" }");

assertFalse(referencingTwoSchema.validate(one).isEmpty());
assertTrue(referencingTwoSchema.validate(two).isEmpty());
assertFalse(referencingTwoSchema.validate(three).isEmpty());
}

@Test
public void uriThatPointsToOneShouldOnlyValidateOne_Uri() throws Exception {
JsonSchema oneSchema = factory.getSchema(new URI("resource:schema/issue619.json#/definitions/one"));

assertTrue(oneSchema.validate(one).isEmpty());
assertFalse(oneSchema.validate(two).isEmpty());
assertFalse(oneSchema.validate(three).isEmpty());
}

@Test
public void uriThatPointsToNodeThatInTurnReferencesOneShouldOnlyValidateOne_Ref() {
JsonSchema referencingTwoSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#/definitions/refToOne\" }");

assertTrue(referencingTwoSchema.validate(one).isEmpty());
assertFalse(referencingTwoSchema.validate(two).isEmpty());
assertFalse(referencingTwoSchema.validate(three).isEmpty());
}

@Test
public void uriThatPointsToNodeThatInTurnReferencesOneShouldOnlyValidateOne_Uri() throws Exception {
JsonSchema oneSchema = factory.getSchema(new URI("resource:schema/issue619.json#/definitions/refToOne"));

assertTrue(oneSchema.validate(one).isEmpty());
assertFalse(oneSchema.validate(two).isEmpty());
assertFalse(oneSchema.validate(three).isEmpty());
}

@Test
public void uriThatPointsToSchemaWithIdThatHasDifferentUri_Ref() throws Exception {
runLocalServer(() -> {
JsonNode oneArray = getJsonNodeFromStringContent("[[1]]");
JsonNode textArray = getJsonNodeFromStringContent("[[\"a\"]]");

JsonSchema schemaWithIdFromRef = factory.getSchema("{ \"$ref\": \"resource:draft4/refRemote.json#/3/schema\" }");
assertTrue(schemaWithIdFromRef.validate(oneArray).isEmpty());
assertFalse(schemaWithIdFromRef.validate(textArray).isEmpty());
});
}

@Test
public void uriThatPointsToSchemaWithIdThatHasDifferentUri_Uri() throws Exception {
runLocalServer(() -> {
JsonNode oneArray = getJsonNodeFromStringContent("[[1]]");
JsonNode textArray = getJsonNodeFromStringContent("[[\"a\"]]");

JsonSchema schemaWithIdFromUri = factory.getSchema(new URI("resource:draft4/refRemote.json#/3/schema"));
assertTrue(schemaWithIdFromUri.validate(oneArray).isEmpty());
assertFalse(schemaWithIdFromUri.validate(textArray).isEmpty());
});
}

private interface ThrowingRunnable {
void run() throws Exception;
}

private void runLocalServer(ThrowingRunnable actualTest) throws Exception {
Undertow server = Undertow.builder()
.addHttpListener(1234, "localhost")
.setHandler(resource(new FileResourceManager(
new File("./src/test/resources/remotes"), 100)))
.build();
try {
server.start();

actualTest.run();

} finally {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
server.stop();
}
}

@Test
public void uriThatPointsToSchemaThatDoesNotExistShouldFail_Ref() {
JsonSchema referencingNonexistentSchema = factory.getSchema("{ \"$ref\": \"resource:data/schema-that-does-not-exist.json#/definitions/something\" }");

assertThrows(JsonSchemaException.class, () -> referencingNonexistentSchema.validate(one));
}

@Test
public void uriThatPointsToSchemaThatDoesNotExistShouldFail_Uri() {
assertThrows(JsonSchemaException.class, () -> factory.getSchema(new URI("resource:data/schema-that-does-not-exist.json#/definitions/something")));
}

@Test
public void uriThatPointsToNodeThatDoesNotExistShouldFail_Ref() {
JsonSchema referencingNonexistentSchema = factory.getSchema("{ \"$ref\": \"resource:schema/issue619.json#/definitions/node-that-does-not-exist\" }");

assertThrows(JsonSchemaException.class, () -> referencingNonexistentSchema.validate(one));
}

@Test
public void uriThatPointsToNodeThatDoesNotExistShouldFail_Uri() {
assertThrows(JsonSchemaException.class, () -> factory.getSchema(new URI("resource:schema/issue619.json#/definitions/node-that-does-not-exist")));
}
}
25 changes: 25 additions & 0 deletions src/test/resources/schema/issue619.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "resource:schema/issue619.json",
"oneOf": [
{
"$ref": "#/definitions/one"
},
{
"$ref": "#/definitions/two"
}
],
"definitions": {
"one": {
"type": "integer",
"enum": [1]
},
"two": {
"type": "integer",
"enum": [2]
},
"refToOne": {
"$ref": "#/definitions/one"
}
}
}

0 comments on commit 527fe6b

Please sign in to comment.