Repository: networknt/json-schema-validator
Pull request #670 (2023-03-03 18:00:05)

issue668: handle references to yaml sub-schemas
fixes https://github.com/networknt/json-schema-validator/issues/668

notes:
* requires dependency on `jackson-dataformat-yaml`
* determination is based on extension in a URI (`.yml` or `.yaml`); if there's no extension, it's assumed to be json

codecov-commenter: # [Codecov](https://codecov.io/gh/networknt/json-schema-validator/pull/670?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) Report
> Merging [#670](https://codecov.io/gh/networknt/json-schema-validator/pull/670?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) (0815871) into [master](https://codecov.io/gh/networknt/json-schema-validator/commit/5b03a984372cddb5c889db91e4ded686aac69b89?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) (5b03a98) will **increase** coverage by `0.06%`.
> The diff coverage is `85.71%`.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt)

```diff
@@             Coverage Diff              @@
##             master     #670      +/-   ##
============================================
+ Coverage     71.38%   71.44%   +0.06%     
- Complexity      997     1002       +5     
============================================
  Files           100      100              
  Lines          3729     3744      +15     
  Branches        779      783       +4     
============================================
+ Hits           2662     2675      +13     
- Misses          788      789       +1     
- Partials        279      280       +1     
```


| [Impacted Files](https://codecov.io/gh/networknt/json-schema-validator/pull/670?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) | Coverage Δ | |
|---|---|---|
| [...n/java/com/networknt/schema/JsonSchemaFactory.java](https://codecov.io/gh/networknt/json-schema-validator/pull/670?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9Kc29uU2NoZW1hRmFjdG9yeS5qYXZh) | `80.40% <85.71%> (+0.51%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt)


Patch:
diff --git a/pom.xml b/pom.xml
index 8c3b15b..4bac3cc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,11 @@
             <artifactId>jackson-databind</artifactId>
             <version>${version.jackson}</version>
         </dependency>
+        <dependency>
+          <groupId>com.fasterxml.jackson.dataformat</groupId>
+          <artifactId>jackson-dataformat-yaml</artifactId>
+          <version>${version.jackson}</version>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index 0f25a5b..6ac18bc 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -18,6 +18,7 @@ package com.networknt.schema;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 import com.networknt.schema.uri.*;
 import com.networknt.schema.urn.URNFactory;
 import org.slf4j.Logger;
@@ -41,6 +42,7 @@ public class JsonSchemaFactory {
 
     public static class Builder {
         private ObjectMapper objectMapper = new ObjectMapper();
+        private YAMLMapper yamlMapper = new YAMLMapper();
         private String defaultMetaSchemaURI;
         private final Map<String, URIFactory> uriFactoryMap = new HashMap<String, URIFactory>();
         private final Map<String, URIFetcher> uriFetcherMap = new HashMap<String, URIFetcher>();
@@ -77,6 +79,11 @@ public class JsonSchemaFactory {
             return this;
         }
 
+        public Builder yamlMapper(final YAMLMapper yamlMapper) {
+            this.yamlMapper = yamlMapper;
+            return this;
+        }
+
         public Builder defaultMetaSchemaURI(final String defaultMetaSchemaURI) {
             this.defaultMetaSchemaURI = defaultMetaSchemaURI;
             return this;
@@ -154,6 +161,7 @@ public class JsonSchemaFactory {
             // create builtin keywords with (custom) formats.
             return new JsonSchemaFactory(
                     objectMapper == null ? new ObjectMapper() : objectMapper,
+                    yamlMapper == null ? new YAMLMapper(): yamlMapper,
                     defaultMetaSchemaURI,
                     new URISchemeFactory(uriFactoryMap),
                     new URISchemeFetcher(uriFetcherMap),
@@ -166,7 +174,8 @@ public class JsonSchemaFactory {
         }
     }
 
-    private final ObjectMapper mapper;
+    private final ObjectMapper jsonMapper;
+    private final YAMLMapper yamlMapper;
     private final String defaultMetaSchemaURI;
     private final URISchemeFactory uriFactory;
     private final URISchemeFetcher uriFetcher;
@@ -179,7 +188,8 @@ public class JsonSchemaFactory {
 
 
     private JsonSchemaFactory(
-            final ObjectMapper mapper,
+            final ObjectMapper jsonMapper,
+            final YAMLMapper yamlMapper,
             final String defaultMetaSchemaURI,
             final URISchemeFactory uriFactory,
             final URISchemeFetcher uriFetcher,
@@ -188,8 +198,10 @@ public class JsonSchemaFactory {
             final Map<String, String> uriMap,
             final boolean forceHttps,
             final boolean removeEmptyFragmentSuffix) {
-        if (mapper == null) {
+        if (jsonMapper == null) {
             throw new IllegalArgumentException("ObjectMapper must not be null");
+        } else if (yamlMapper == null) {
+            throw new IllegalArgumentException("YAMLMapper must not be null");
         } else if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) {
             throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty");
         } else if (uriFactory == null) {
@@ -203,7 +215,8 @@ public class JsonSchemaFactory {
         } else if (uriMap == null) {
             throw new IllegalArgumentException("URL Mappings must not be null");
         }
-        this.mapper = mapper;
+        this.jsonMapper = jsonMapper;
+        this.yamlMapper = yamlMapper;
         this.defaultMetaSchemaURI = defaultMetaSchemaURI;
         this.uriFactory = uriFactory;
         this.uriFetcher = uriFetcher;
@@ -274,7 +287,8 @@ public class JsonSchemaFactory {
         Builder builder = builder()
                 .addMetaSchemas(blueprint.jsonMetaSchemas.values())
                 .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI)
-                .objectMapper(blueprint.mapper)
+                .objectMapper(blueprint.jsonMapper)
+                .yamlMapper(blueprint.yamlMapper)
                 .addUriMappings(blueprint.uriMap);
 
         for (Map.Entry<String, URIFactory> entry : blueprint.uriFactory.getURIFactories().entrySet()) {
@@ -319,7 +333,7 @@ public class JsonSchemaFactory {
 
     public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) {
         try {
-            final JsonNode schemaNode = mapper.readTree(schema);
+            final JsonNode schemaNode = jsonMapper.readTree(schema);
             return newJsonSchema(null, schemaNode, config);
         } catch (IOException ioe) {
             logger.error("Failed to load json schema!", ioe);
@@ -333,7 +347,7 @@ public class JsonSchemaFactory {
 
     public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidatorsConfig config) {
         try {
-            final JsonNode schemaNode = mapper.readTree(schemaStream);
+            final JsonNode schemaNode = jsonMapper.readTree(schemaStream);
             return newJsonSchema(null, schemaNode, config);
         } catch (IOException ioe) {
             logger.error("Failed to load json schema!", ioe);
@@ -370,7 +384,14 @@ public class JsonSchemaFactory {
 
             try {
                 inputStream = this.uriFetcher.fetch(mappedUri);
-                final JsonNode schemaNode = mapper.readTree(inputStream);
+
+                final JsonNode schemaNode;
+                if (isYaml(mappedUri)) {
+                    schemaNode = yamlMapper.readTree(inputStream);
+                } else {
+                    schemaNode = jsonMapper.readTree(inputStream);
+                }
+
                 final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode);
 
                 JsonSchema jsonSchema;
@@ -428,6 +449,19 @@ public class JsonSchemaFactory {
         return result;
     }
 
+    private boolean isYaml(final URI schemaUri) {
+        final String schemeSpecificPart = schemaUri.getSchemeSpecificPart();
+        final int idx = schemeSpecificPart.lastIndexOf('.');
+
+        if (idx == -1) {
+            // no extension; assume json
+            return false;
+        }
+
+        final String extension = schemeSpecificPart.substring(idx);
+        return (".yml".equals(extension) || ".yaml".equals(extension));
+    }
+
     static protected String normalizeMetaSchemaUri(String u, boolean forceHttps, boolean removeEmptyFragmentSuffix) {
         try {
             URI uri = new URI(u);


Test patch:
diff --git a/src/test/java/com/networknt/schema/Issue668Test.java b/src/test/java/com/networknt/schema/Issue668Test.java
new file mode 100644
index 0000000..bdecac7
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue668Test.java
@@ -0,0 +1,35 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+
+class Issue668Test {
+    protected JsonSchema getJsonSchemaFromStreamContent(InputStream schemaContent) throws Exception {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        YAMLMapper mapper = new YAMLMapper();
+        JsonNode node = mapper.readTree(schemaContent);
+        return factory.getSchema(node);
+    }
+
+    protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readTree(content);
+    }
+
+    @Test
+    void shouldHandleReferencesToYaml() throws Exception {
+        String schemaPath = "/schema/issue668.yml";
+        String dataPath = "/data/issue668.json";
+        InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath);
+        JsonSchema schema = getJsonSchemaFromStreamContent(schemaInputStream);
+        InputStream dataInputStream = getClass().getResourceAsStream(dataPath);
+        JsonNode node = getJsonNodeFromStreamContent(dataInputStream);
+        MatcherAssert.assertThat(schema.validate(node), Matchers.empty());
+    }
+}
diff --git a/src/test/resources/data/issue668.json b/src/test/resources/data/issue668.json
new file mode 100644
index 0000000..12f9330
--- /dev/null
+++ b/src/test/resources/data/issue668.json
@@ -0,0 +1,5 @@
+{
+  "sub1": {},
+  "sub2": {},
+  "sub3": {}
+}
\ No newline at end of file
diff --git a/src/test/resources/schema/issue668-sub1.yml b/src/test/resources/schema/issue668-sub1.yml
new file mode 100644
index 0000000..d73ddd1
--- /dev/null
+++ b/src/test/resources/schema/issue668-sub1.yml
@@ -0,0 +1,1 @@
+type: object
\ No newline at end of file
diff --git a/src/test/resources/schema/issue668-sub2.yaml b/src/test/resources/schema/issue668-sub2.yaml
new file mode 100644
index 0000000..d73ddd1
--- /dev/null
+++ b/src/test/resources/schema/issue668-sub2.yaml
@@ -0,0 +1,1 @@
+type: object
\ No newline at end of file
diff --git a/src/test/resources/schema/issue668-sub3 b/src/test/resources/schema/issue668-sub3
new file mode 100644
index 0000000..61ef6e4
--- /dev/null
+++ b/src/test/resources/schema/issue668-sub3
@@ -0,0 +1,1 @@
+{"type": "object"}
\ No newline at end of file
diff --git a/src/test/resources/schema/issue668.yml b/src/test/resources/schema/issue668.yml
new file mode 100644
index 0000000..e69f514
--- /dev/null
+++ b/src/test/resources/schema/issue668.yml
@@ -0,0 +1,9 @@
+$id: resource:/schema/issue668
+type: object
+properties:
+  sub1:
+    $ref: issue668-sub1.yml
+  sub2:
+    $ref: issue668-sub2.yaml
+  sub3:
+    $ref: issue668-sub3
\ No newline at end of file