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
75 changes: 68 additions & 7 deletions src/main/java/com/networknt/schema/SchemaRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,10 @@ public SchemaLoader getSchemaLoader() {
* @return the schema
*/
protected Schema newSchema(SchemaLocation schemaUri, JsonNode schemaNode) {
SchemaLocation schemaLocation = getSchemaLocation(schemaUri);
validateSchemaNodeNotNull(schemaLocation, schemaNode);
final SchemaContext schemaContext = createSchemaContext(schemaNode);
Schema jsonSchema = doCreate(schemaContext, getSchemaLocation(schemaUri),
schemaNode, null, false);
Schema jsonSchema = doCreate(schemaContext, schemaLocation, schemaNode, null, false);
Comment thread
stevehu marked this conversation as resolved.
preload(jsonSchema);
return jsonSchema;
}
Expand Down Expand Up @@ -507,10 +508,46 @@ public Schema create(SchemaContext schemaContext, SchemaLocation schemaLocation,

private Schema doCreate(SchemaContext schemaContext, SchemaLocation schemaLocation,
JsonNode schemaNode, Schema parentSchema, boolean suppressSubSchemaRetrieval) {
return Schema.from(withDialect(schemaContext, schemaNode), schemaLocation, schemaNode,
return doCreate(schemaContext, schemaLocation, schemaNode, parentSchema, suppressSubSchemaRetrieval, true);
}

private Schema doCreate(SchemaContext schemaContext, SchemaLocation schemaLocation,
JsonNode schemaNode, Schema parentSchema, boolean suppressSubSchemaRetrieval,
boolean validateSchemaNodeType) {
validateSchemaNodeNotNull(schemaLocation, schemaNode);
SchemaContext schemaContextToUse = withDialect(schemaContext, schemaNode);
if (validateSchemaNodeType) {
validateSchemaNodeType(schemaLocation, schemaNode, schemaContextToUse);
}
return Schema.from(schemaContextToUse, schemaLocation, schemaNode,
parentSchema, suppressSubSchemaRetrieval);
Comment thread
stevehu marked this conversation as resolved.
Comment thread
stevehu marked this conversation as resolved.
}

private void validateSchemaNodeNotNull(SchemaLocation schemaLocation, JsonNode schemaNode) {
if (schemaNode == null) {
throw new SchemaException("Schema at " + schemaLocation + " must not be null");
}
}

private void validateSchemaNodeType(SchemaLocation schemaLocation, JsonNode schemaNode,
SchemaContext schemaContext) {
if (schemaNode.isObject()) {
return;
}
if (schemaNode.isBoolean() && supportsBooleanSchema(schemaContext)) {
return;
}
String expected = supportsBooleanSchema(schemaContext) ? "object or boolean" : "object";
throw new SchemaException("Schema at " + schemaLocation + " must be " + expected + " but was "
+ schemaNode.getNodeType());
}

private boolean supportsBooleanSchema(SchemaContext schemaContext) {
return schemaContext != null
&& schemaContext.getDialect().getSpecificationVersion().getOrder() >= SpecificationVersion.DRAFT_6
.getOrder();
}

/**
* Determines the schema context to use for the schema given the parent schema
* context.
Expand Down Expand Up @@ -657,6 +694,7 @@ public Schema getSchema(final InputStream schemaStream, InputFormat inputFormat)
*/
public Schema getSchema(final SchemaLocation schemaUri) {
Schema schema = loadSchema(schemaUri);
validateSchemaNodeType(schema.getSchemaLocation(), schema.getSchemaNode(), schema.getSchemaContext());
preload(schema);
return schema;
}
Expand Down Expand Up @@ -722,6 +760,20 @@ public Schema getSchema(final JsonNode jsonNode) {
* @return the schema
*/
public Schema loadSchema(final SchemaLocation schemaUri) {
return loadSchema(schemaUri, true);
}

/**
* Loads the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the
* retrieval IRI.
* @param validateLoadedSchema true to validate that the loaded node is a schema;
* false when loading a document container to resolve a
* fragment before using it as a schema
* @return the schema
*/
public Schema loadSchema(final SchemaLocation schemaUri, boolean validateLoadedSchema) {
if (schemaCacheEnabled) {
// ConcurrentHashMap computeIfAbsent does not allow calls that result in a
// recursive update to the map.
Expand All @@ -731,19 +783,28 @@ public Schema loadSchema(final SchemaLocation schemaUri) {
synchronized (this) { // acquire lock on shared registry object to prevent deadlock
cachedUriSchema = schemaCache.get(schemaUri);
if (cachedUriSchema == null) {
cachedUriSchema = getMappedSchema(schemaUri);
cachedUriSchema = validateLoadedSchema ? getMappedSchema(schemaUri)
: getMappedSchema(schemaUri, false);
if (cachedUriSchema != null) {
schemaCache.put(schemaUri, cachedUriSchema);
}
}
}
}
if (validateLoadedSchema && cachedUriSchema != null) {
validateSchemaNodeType(cachedUriSchema.getSchemaLocation(), cachedUriSchema.getSchemaNode(),
cachedUriSchema.getSchemaContext());
}
return cachedUriSchema;
}
return getMappedSchema(schemaUri);
return validateLoadedSchema ? getMappedSchema(schemaUri) : getMappedSchema(schemaUri, false);
}

protected Schema getMappedSchema(final SchemaLocation schemaUri) {
return getMappedSchema(schemaUri, true);
}

protected Schema getMappedSchema(final SchemaLocation schemaUri, boolean validateLoadedSchema) {
InputStreamSource inputStreamSource = this.schemaLoader.getSchemaResource(schemaUri.getAbsoluteIri());
if (inputStreamSource != null) {
try (InputStream inputStream = inputStreamSource.getInputStream()) {
Expand All @@ -762,13 +823,13 @@ protected Schema getMappedSchema(final SchemaLocation schemaUri) {
// Schema without fragment
SchemaContext schemaContext = new SchemaContext(dialect, this);
return doCreate(schemaContext, schemaUri, schemaNode, null,
true /* retrieved via id, resolving will not change anything */);
true /* retrieved via id, resolving will not change anything */, validateLoadedSchema);
} else {
// Schema with fragment pointing to sub schema
final SchemaContext schemaContext = createSchemaContext(schemaNode);
SchemaLocation documentLocation = new SchemaLocation(schemaUri.getAbsoluteIri());
Schema document = doCreate(schemaContext, documentLocation, schemaNode, null,
false);
false, false);
return document.getRefSchema(schemaUri.getFragment());
}
} catch (IOException e) {
Expand Down
40 changes: 21 additions & 19 deletions src/main/java/com/networknt/schema/keyword/RefValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,30 @@ static SchemaRef getRefSchema(Schema parentSchema, SchemaContext schemaContext,
refUri = refValue;
}

// This will determine the correct absolute uri for the refUri. This decision will take into
// account the current uri of the parent schema.
String schemaUriFinal = resolve(parentSchema, refUri);
SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal);
// This should retrieve schemas regardless of the protocol that is in the uri.
return new SchemaRef(getSupplier(() -> {
Schema schemaResource = schemaContext.getSchemaResources().get(schemaUriFinal);
if (schemaResource == null) {
schemaResource = schemaContext.getSchemaRegistry().loadSchema(schemaLocation);
if (schemaResource != null) {
copySchemaResources(schemaContext, schemaResource);
}
}
// This will determine the correct absolute uri for the refUri. This decision will take into
// account the current uri of the parent schema.
String schemaUriFinal = resolve(parentSchema, refUri);
SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal);
String fragment = index < 0 ? null : refValue.substring(index);
boolean validateLoadedSchema = fragment == null || !SchemaLocation.Fragment.isJsonPointerFragment(fragment);
// This should retrieve schemas regardless of the protocol that is in the uri.
return new SchemaRef(getSupplier(() -> {
Schema schemaResource = schemaContext.getSchemaResources().get(schemaUriFinal);
if (schemaResource == null) {
schemaResource = schemaContext.getSchemaRegistry().loadSchema(schemaLocation, validateLoadedSchema);
if (schemaResource != null) {
copySchemaResources(schemaContext, schemaResource);
}
}
if (index < 0) {
if (schemaResource == null) {
return null;
}
return schemaResource;
} else {
String newRefValue = refValue.substring(index);
String find = schemaLocation.getAbsoluteIri() + newRefValue;
Schema findSchemaResource = schemaContext.getSchemaResources().get(find);
}
return schemaResource;
} else {
String newRefValue = fragment;
String find = schemaLocation.getAbsoluteIri() + newRefValue;
Schema findSchemaResource = schemaContext.getSchemaResources().get(find);
if (findSchemaResource == null) {
findSchemaResource = schemaContext.getDynamicAnchors().get(find);
}
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/com/networknt/schema/DependentRequiredTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class DependentRequiredTest {
" \"$schema\":\"https://json-schema.org/draft/2019-09/schema\"," +
" \"type\": \"object\"," +
" \"properties\": {" +
" \"optional\": \"string\"," +
" \"requiredWhenOptionalPresent\": \"string\"" +
" \"optional\": { \"type\": \"string\" }," +
" \"requiredWhenOptionalPresent\": { \"type\": \"string\" }" +
" }," +
" \"dependentRequired\": {" +
" \"optional\": [ \"requiredWhenOptionalPresent\" ]," +
Expand Down Expand Up @@ -63,4 +63,4 @@ private static List<Error> whenValidate(String content) throws JacksonException
return schema.validate(mapper.readTree(content));
}

}
}
141 changes: 141 additions & 0 deletions src/test/java/com/networknt/schema/Issue1174Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2026 the original author or authors.
*
* 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;
Comment thread
Copilot marked this conversation as resolved.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.node.JsonNodeFactory;

class Issue1174Test {
@Test
void textNodeShouldNotBeAcceptedAsRootSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);

SchemaException exception = assertThrows(SchemaException.class,
() -> registry.getSchema(JsonNodeFactory.instance.textNode("false")));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void textNodeContainingJsonShouldNotBeAcceptedAsRootSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);

SchemaException exception = assertThrows(SchemaException.class,
() -> registry.getSchema(JsonNodeFactory.instance.textNode("{\"type\":\"string\"}")));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void textNodeShouldNotBeAcceptedAsLoadedRootSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7,
builder -> builder.schemas(Collections.singletonMap("https://www.example.org/text-schema", "\"false\"")));

SchemaException exception = assertThrows(SchemaException.class,
() -> registry.getSchema(SchemaLocation.of("https://www.example.org/text-schema")));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void textNodeShouldNotBeAcceptedAsReferencedRootSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7,
builder -> builder.schemas(Collections.singletonMap("https://www.example.org/text-schema", "\"false\"")));
Schema schema = registry.getSchema("{\"$ref\":\"https://www.example.org/text-schema\"}");

SchemaException exception = assertThrows(SchemaException.class, () -> schema.validate("42", InputFormat.JSON));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void textNodeShouldNotBeAcceptedAsReferencedDocumentFragmentSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7,
builder -> builder.schemas(Collections.singletonMap("https://www.example.org/text-schema", "\"false\"")));
Schema schema = registry.getSchema("{\"$ref\":\"https://www.example.org/text-schema#\"}");

SchemaException exception = assertThrows(SchemaException.class, () -> schema.validate("42", InputFormat.JSON));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void textNodeShouldNotBeAcceptedAsReferencedAnchorSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7,
builder -> builder.schemas(Collections.singletonMap("https://www.example.org/text-schema", "\"false\"")));
Schema schema = registry.getSchema("{\"$ref\":\"https://www.example.org/text-schema#myAnchor\"}");

SchemaException exception = assertThrows(SchemaException.class, () -> schema.validate("42", InputFormat.JSON));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void textNodeShouldNotBeAcceptedAsSubSchema() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);

SchemaException exception = assertThrows(SchemaException.class,
() -> registry.getSchema("{\"type\":\"object\",\"properties\":{\"a\":\"false\"}}"));

assertTrue(exception.getMessage().contains("must be object or boolean"));
assertTrue(exception.getMessage().contains("STRING"));
}

@Test
void booleanSchemaShouldBeAcceptedForDraft7() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
Schema schema = registry.getSchema("false");

List<Error> errors = schema.validate("42", InputFormat.JSON);

assertEquals(1, errors.size());
}

@Test
void loadedBooleanSchemaShouldBeAcceptedForDraft7() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7,
builder -> builder.schemas(Collections.singletonMap("https://www.example.org/false-schema", "false")));
Schema schema = registry.getSchema(SchemaLocation.of("https://www.example.org/false-schema"));

List<Error> errors = schema.validate("42", InputFormat.JSON);

assertEquals(1, errors.size());
}

@Test
void booleanSchemaShouldNotBeAcceptedForDraft4() {
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_4);

SchemaException exception = assertThrows(SchemaException.class, () -> registry.getSchema("false"));

assertTrue(exception.getMessage().contains("must be object"));
assertTrue(exception.getMessage().contains("BOOLEAN"));
}
}
12 changes: 6 additions & 6 deletions src/test/java/com/networknt/schema/SchemaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ void concurrency() throws Exception {
+ " \"name\": {\r\n"
+ " \"type\": \"string\",\r\n"
+ " \"description\": \"The name\"\r\n"
+ " },\r\n"
+ " \"required\": [\r\n"
+ " \"name\"\r\n"
+ " ]\r\n"
+ " }\r\n"
+ "}";
+ " }\r\n"
+ " },\r\n"
+ " \"required\": [\r\n"
+ " \"name\"\r\n"
+ " ]\r\n"
+ "}";
String inputData = "{\r\n"
+ " \"name\": 1\r\n"
+ "}";
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

class UnknownMetaSchemaTest {

private final String schema1 = "{\"$schema\":\"http://json-schema.org/draft-07/schema\",\"title\":\"thingModel\",\"description\":\"description of thing\",\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"integer\"},\"required\":[\"data\"]}}";
private final String schema2 = "{\"$schema\":\"https://json-schema.org/draft-07/schema\",\"title\":\"thingModel\",\"description\":\"description of thing\",\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"integer\"},\"required\":[\"data\"]}}";
private final String schema3 = "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"thingModel\",\"description\":\"description of thing\",\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"integer\"},\"required\":[\"data\"]}}";
private final String schema1 = "{\"$schema\":\"http://json-schema.org/draft-07/schema\",\"title\":\"thingModel\",\"description\":\"description of thing\",\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"integer\"}},\"required\":[\"data\"]}";
private final String schema2 = "{\"$schema\":\"https://json-schema.org/draft-07/schema\",\"title\":\"thingModel\",\"description\":\"description of thing\",\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"integer\"}},\"required\":[\"data\"]}";
private final String schema3 = "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"thingModel\",\"description\":\"description of thing\",\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"integer\"}},\"required\":[\"data\"]}";

private final String json = "{\"data\":1}";

Expand Down
Loading
Loading