diff --git a/README.md b/README.md index 3799cdd68..d7378e4bd 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ The specification allows for the `$schema` keyword not to be specified, in which The following example creates a `SchemaRegistry` that does not specify a default dialect and will throw a `MissingSchemaKeywordException` if the schema does not specify a dialect using the `$schema` keyword. ```java -SchemaRegistry registry = SchemaRegistry.builder().dialectRegistry(new BasicDialectRegistry(Dialects.getDraft202012())).build(); +SchemaRegistry registry = SchemaRegistry.withDefaultDialectId(null); ``` ### Results and output formats diff --git a/src/main/java/com/networknt/schema/SchemaRegistry.java b/src/main/java/com/networknt/schema/SchemaRegistry.java index cbbd6c669..d5f4acfee 100644 --- a/src/main/java/com/networknt/schema/SchemaRegistry.java +++ b/src/main/java/com/networknt/schema/SchemaRegistry.java @@ -282,6 +282,26 @@ public static SchemaRegistry withDefaultDialect(SpecificationVersion specificati * Creates a new schema registry with a default schema dialect. The schema * dialect will only be used if the input does not specify a $schema. *

+ * If the dialectId is null then the $schema is mandatory. + *

+ * This uses a dialect registry that contains all the supported standard + * specification dialects, Draft 4, Draft 6, Draft 7, Draft 2019-09 and Draft + * 2020-12. + * + * @param dialectId the default dialect id used when the schema does not + * specify the $schema keyword + * @return the factory + */ + public static SchemaRegistry withDefaultDialectId(String dialectId) { + return withDefaultDialectId(dialectId, null); + } + + /** + * Creates a new schema registry with a default schema dialect. The schema + * dialect will only be used if the input does not specify a $schema. + *

+ * If the dialectId is null then the $schema is mandatory. + *

* This uses a dialect registry that contains all the supported standard * specification dialects, Draft 4, Draft 6, Draft 7, Draft 2019-09 and Draft * 2020-12. diff --git a/src/main/java/com/networknt/schema/SpecificationVersion.java b/src/main/java/com/networknt/schema/SpecificationVersion.java index 33bd3752e..286c3f1fc 100644 --- a/src/main/java/com/networknt/schema/SpecificationVersion.java +++ b/src/main/java/com/networknt/schema/SpecificationVersion.java @@ -19,6 +19,8 @@ import com.networknt.schema.dialect.DialectId; +import tools.jackson.databind.JsonNode; + /** * The version of the JSON Schema specification that defines the standard * dialects. @@ -90,4 +92,20 @@ public static Optional fromDialectId(String dialectId) { } return Optional.empty(); } + + /** + * Gets the specification version that matches the dialect id indicated by + * $schema keyword. The dialect id is an IRI that identifies the meta schema + * used to validate the dialect. + * + * @param schemaNode the schema + * @return the specification version if it matches the dialect id + */ + public static Optional fromSchemaNode(JsonNode schemaNode) { + JsonNode schema = schemaNode.get("$schema"); + if (schema != null && schema.isString()) { + return fromDialectId(schema.stringValue()); + } + return Optional.empty(); + } } diff --git a/src/test/java/com/networknt/schema/SchemaRegistryTest.java b/src/test/java/com/networknt/schema/SchemaRegistryTest.java index 1afff4380..15d53b3c6 100644 --- a/src/test/java/com/networknt/schema/SchemaRegistryTest.java +++ b/src/test/java/com/networknt/schema/SchemaRegistryTest.java @@ -19,7 +19,9 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -108,6 +110,14 @@ void noDefaultDialectButSchemaSpecified() { }); } + @Test + void noDefaultDialectWithDialectId() { + SchemaRegistry registry = SchemaRegistry.withDefaultDialectId(null); + assertThrows(MissingSchemaKeywordException.class, () -> { + registry.getSchema("{\"type\":\"object\"}"); + }); + } + @Test void noDefaultDialectButSchemaSpecifiedButNotInRegistry() { SchemaRegistry registry = SchemaRegistry.builder() @@ -116,4 +126,37 @@ void noDefaultDialectButSchemaSpecifiedButNotInRegistry() { registry.getSchema("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"object\"}"); }); } + + @Test + void noDialectReferredByParentShouldDefaultToDefaultDialect() { + String schema = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"key\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"description\": \"The unique identifier or name (key) for the pair.\"\r\n" + + " },\r\n" + + " \"value\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"description\": \"The associated data (value) for the key.\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"key\",\r\n" + + " \"value\"\r\n" + + " ],\r\n" + + " \"additionalProperties\": false\r\n" + + "}"; + Map schemas = new HashMap<>(); + schemas.put("https://example.org/schema", schema); + SchemaRegistry registry = SchemaRegistry.withDefaultDialect(Dialects.getDraft4(), builder -> builder.schemas(schemas)); + Schema result = registry.getSchema("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"object\",\"$ref\":\"https://example.org/schema\"}"); + String input = "{\r\n" + + " \"key\": \"user_id\",\r\n" + + " \"value\": \"123456\"\r\n" + + "}"; + result.validate(input, InputFormat.JSON); + Schema nested = registry.getSchema(SchemaLocation.of("https://example.org/schema")); + assertEquals(Dialects.getDraft4(), nested.getSchemaContext().getDialect()); + } } diff --git a/src/test/java/com/networknt/schema/SpecificationVersionTest.java b/src/test/java/com/networknt/schema/SpecificationVersionTest.java new file mode 100644 index 000000000..631948e90 --- /dev/null +++ b/src/test/java/com/networknt/schema/SpecificationVersionTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +class SpecificationVersionTest { + @Test + void fromSchemaNode() { + String schema = "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"object\",\"$ref\":\"https://example.org/schema\"}"; + JsonNode schemaNode = JsonMapper.shared().readTree(schema); + assertEquals(SpecificationVersionDetector.detectOptionalVersion(schemaNode, false), + SpecificationVersion.fromSchemaNode(schemaNode)); + } + + @Test + void fromSchemaNodeMissing() { + String schema = "{\"type\":\"object\",\"$ref\":\"https://example.org/schema\"}"; + JsonNode schemaNode = JsonMapper.shared().readTree(schema); + assertEquals(SpecificationVersionDetector.detectOptionalVersion(schemaNode, false), + SpecificationVersion.fromSchemaNode(schemaNode)); + } +}