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));
+ }
+}