Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
}
}