Repository: networknt/json-schema-validator (java)

Issue:
NPE with oneOf and custom URI Fetcher or Factory
Hi

I tried to update my project from 1.0.52 to 1.0.66 and now get a NPE with schemas that use `oneOf` with a custom URI fetcher. Or at least I think it's related to the URI fetcher, since `oneOf` with `$ref` to a definition, etc. still work fine.

```
java.lang.NullPointerException: Cannot invoke "com.networknt.schema.JsonSchema.getSchemaNode()" because the return value of "com.networknt.schema.JsonSchema.getParentSchema()" is null
    at com.networknt.schema.utils.JsonNodeUtil.equalsToSchemaType(JsonNodeUtil.java:88)
    at com.networknt.schema.TypeValidator.validate(TypeValidator.java:120)
    at com.networknt.schema.JsonSchema.validate(JsonSchema.java:285)
    at com.networknt.schema.JsonSchemaRef.validate(JsonSchemaRef.java:38)
    at com.networknt.schema.RefValidator.validate(RefValidator.java:139)
    at com.networknt.schema.JsonSchema.validate(JsonSchema.java:285)
    at com.networknt.schema.OneOfValidator.validate(OneOfValidator.java:160)
    at com.networknt.schema.JsonSchema.validate(JsonSchema.java:285)
    at com.networknt.schema.PropertiesValidator.validate(PropertiesValidator.java:68)
    at com.networknt.schema.JsonSchema.validate(JsonSchema.java:285)
    at com.networknt.schema.BaseJsonValidator.validate(BaseJsonValidator.java:114)
    ...
```

The same code does not produce a NPE with release 1.0.57, so I guess it has something to do with the changes in 1.0.58. Here is a test case to reproduce the issue:

```
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.io.CharSource;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import com.networknt.schema.uri.URIFactory;
import com.networknt.schema.uri.URIFetcher;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Set;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class CustomUriTest {
    @Test
    public void customUri() throws Exception {
        /* Given */
        final JsonSchemaFactory factory = buildJsonSchemaFactory();
        final JsonSchema schema = factory.getSchema("""
            {
                "$schema": "https://json-schema.org/draft/2019-09/schema",
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "customAnyOf": {
                        "anyOf": [
                            {"type": "null"},
                            {"$ref": "custom:date"}
                        ]
                    },
                    "customOneOf": {
                        "oneOf": [
                            {"type": "null"},
                            {"$ref": "custom:date"}
                        ]
                    }
                }
            }""");
        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode value = mapper.readTree("""
            {
                "customAnyOf": null,
                "customOneOf": null
            }""");

        /* When */
        final Set<ValidationMessage> errors = schema.validate(value);

        /* Then */
        assertThat(errors.isEmpty(), is(true));
    }

    private JsonSchemaFactory buildJsonSchemaFactory() {
        return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909))
            .uriFetcher(new CustomUriFetcher(), "custom").uriFactory(new CustomUriFactory(), "custom").build();
    }

    private static class CustomUriFetcher implements URIFetcher {
        private static final String SCHEMA = """
            {"$schema": "https://json-schema.org/draft/2019-09/schema","$id":"custom:date","type":"string","format":"date"}""";

        @Override
        public InputStream fetch(final URI uri) throws IOException {
            return CharSource.wrap(SCHEMA).asByteSource(Charsets.UTF_8).openStream();
        }
    }

    private static class CustomUriFactory implements URIFactory {
        @Override
        public URI create(final String uri) {
            return URI.create(uri);
        }

        @Override
        public URI create(final URI baseURI, final String segment) {
            return baseURI.resolve(segment);
        }
    }
}
```

`anyOf` is just included here because I was interested to know if it's affected and the answer is no.

Thanks
Chris


Pull request:
fixes #509 NPE with oneOf and custom URI Fetcher or Factory
@rongyj I have added additional logic to ensure that no null pointer exception will be thrown. However, if parentSchema or parentSchema.getParentSchema is null, the return value will be false which I don't feel comfortable with. I am wondering if you could take a look at the logic and approve the change if you are comfortable. Thanks. 

@chrswk Thanks a lot for your test case. Please review and approve the change. Now the test case is passed after checking the null. If you have other test cases, please add them to ensure the code logic is correct and nothing else is broken. 

Patch:
diff --git a/src/main/java/com/networknt/schema/DateTimeValidator.java b/src/main/java/com/networknt/schema/DateTimeValidator.java
index 420aaac..38f9873 100644
--- a/src/main/java/com/networknt/schema/DateTimeValidator.java
+++ b/src/main/java/com/networknt/schema/DateTimeValidator.java
@@ -40,8 +40,8 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
 
     private static final Pattern RFC3339_PATTERN = Pattern.compile(
             "^(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd
-                    + "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?)?" // 'T'HH:mm:ss.milliseconds
-                    + "([Zz]|([+-])(\\d{2}):?(\\d{2}))?");
+                    + "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?" // 'T'HH:mm:ss.milliseconds
+                    + "(([Zz])|([+-])(\\d{2}):(\\d{2})))?");
 
     public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, String formatName) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DATETIME, validationContext);
@@ -82,12 +82,12 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
         pattern.append("yyyy-MM-dd");
 
         boolean isTimeGiven = matcher.group(4) != null;
-        String timeZoneShiftRegexGroup = matcher.group(9);
-        boolean isTimeZoneShiftGiven = timeZoneShiftRegexGroup != null;
+        boolean isOffsetZuluTime = matcher.group(10) != null;
         String hour = null;
         String minute = null;
         String second = null;
         String milliseconds = null;
+        String timeShiftSign = null;
         String timeShiftHour = null;
         String timeShiftMinute = null;
 
@@ -96,12 +96,6 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
             return false;
         }
 
-        if (!isTimeGiven && isTimeZoneShiftGiven) {
-            logger.error("Invalid date/time format, cannot specify time zone shift" +
-                    " without specifying time: " + string);
-            return false;
-        }
-
         if (isTimeGiven) {
             hour = matcher.group(5);
             minute = matcher.group(6);
@@ -121,16 +115,15 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
                 dateTime.append(milliseconds);
                 pattern.append(".SSS");
             }
-        }
 
-        if (isTimeGiven && isTimeZoneShiftGiven) {
-            if (Character.toUpperCase(timeZoneShiftRegexGroup.charAt(0)) == 'Z') {
+            if (isOffsetZuluTime) {
                 dateTime.append('Z');
                 pattern.append("'Z'");
             } else {
-                timeShiftHour = matcher.group(11);
-                timeShiftMinute = matcher.group(12);
-                dateTime.append(matcher.group(10).charAt(0)).append(timeShiftHour).append(':').append(timeShiftMinute);
+                timeShiftSign = matcher.group(11);
+                timeShiftHour = matcher.group(12);
+                timeShiftMinute = matcher.group(13);
+                dateTime.append(timeShiftSign).append(timeShiftHour).append(':').append(timeShiftMinute);
                 pattern.append("XXX");
             }
         }
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index b8f4baf..e980066 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -48,7 +48,7 @@ public class JsonSchemaFactory {
         private final Map<String, JsonMetaSchema> jsonMetaSchemas = new HashMap<String, JsonMetaSchema>();
         private final Map<String, String> uriMap = new HashMap<String, String>();
         private boolean forceHttps = true;
-		
+        private boolean removeEmptyFragmentSuffix = true;
 
         public Builder() {
             // Adds support for creating {@link URL}s.
@@ -145,6 +145,11 @@ public class JsonSchemaFactory {
             return this;
         }
 
+        public Builder removeEmptyFragmentSuffix(boolean removeEmptyFragmentSuffix) {
+            this.removeEmptyFragmentSuffix = removeEmptyFragmentSuffix;
+            return this;
+        }
+
         public JsonSchemaFactory build() {
             // create builtin keywords with (custom) formats.
             return new JsonSchemaFactory(
@@ -155,7 +160,8 @@ public class JsonSchemaFactory {
                     urnFactory,
                     jsonMetaSchemas,
                     uriMap,
-                    forceHttps
+                    forceHttps,
+                    removeEmptyFragmentSuffix
             );
         }
     }
@@ -169,6 +175,7 @@ public class JsonSchemaFactory {
     private final Map<String, String> uriMap;
     private final ConcurrentMap<URI, JsonSchema> uriSchemaCache = new ConcurrentHashMap<URI, JsonSchema>();
     private final boolean forceHttps;
+    private final boolean removeEmptyFragmentSuffix;
 
 
     private JsonSchemaFactory(
@@ -179,7 +186,8 @@ public class JsonSchemaFactory {
             final URNFactory urnFactory,
             final Map<String, JsonMetaSchema> jsonMetaSchemas,
             final Map<String, String> uriMap,
-            final boolean forceHttps) {
+            final boolean forceHttps,
+            final boolean removeEmptyFragmentSuffix) {
         if (mapper == null) {
             throw new IllegalArgumentException("ObjectMapper must not be null");
         } else if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) {
@@ -203,6 +211,7 @@ public class JsonSchemaFactory {
         this.jsonMetaSchemas = jsonMetaSchemas;
         this.uriMap = uriMap;
         this.forceHttps = forceHttps;
+        this.removeEmptyFragmentSuffix = removeEmptyFragmentSuffix;
     }
 
     /**
@@ -281,7 +290,7 @@ public class JsonSchemaFactory {
 
     private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) {
         final JsonNode uriNode = schemaNode.get("$schema");
-        final String uri = uriNode == null || uriNode.isNull() ? defaultMetaSchemaURI : normalizeMetaSchemaUri(uriNode.textValue(), forceHttps);
+        final String uri = uriNode == null || uriNode.isNull() ? defaultMetaSchemaURI : normalizeMetaSchemaUri(uriNode.textValue(), forceHttps, removeEmptyFragmentSuffix);
         final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.get(uri);
         if (jsonMetaSchema == null) {
             throw new JsonSchemaException("Unknown MetaSchema: " + uri);
@@ -409,12 +418,17 @@ public class JsonSchemaFactory {
         return result;
     }
 
-    static protected String normalizeMetaSchemaUri(String u, boolean forceHttps) {
+    static protected String normalizeMetaSchemaUri(String u, boolean forceHttps, boolean removeEmptyFragmentSuffix) {
         try {
             URI uri = new URI(u);
             String scheme = forceHttps ? "https" : uri.getScheme();
             URI newUri = new URI(scheme, uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null);
-            return newUri.toString();
+
+            if (!removeEmptyFragmentSuffix && u.endsWith("#")) {
+                return newUri + "#";
+            } else {
+                return newUri.toString();
+            }
         } catch (URISyntaxException e) {
             throw new JsonSchemaException("Wrong MetaSchema URI: " + u);
         }
diff --git a/src/main/java/com/networknt/schema/SpecVersionDetector.java b/src/main/java/com/networknt/schema/SpecVersionDetector.java
index 190ab4e..400008a 100644
--- a/src/main/java/com/networknt/schema/SpecVersionDetector.java
+++ b/src/main/java/com/networknt/schema/SpecVersionDetector.java
@@ -39,7 +39,10 @@ public class SpecVersionDetector {
         if (!jsonNode.has(SCHEMA_TAG))
             throw new JsonSchemaException("Schema tag not present");
 
-        String schemaUri = JsonSchemaFactory.normalizeMetaSchemaUri(jsonNode.get(SCHEMA_TAG).asText(), true);
+        final boolean forceHttps = true;
+        final boolean removeEmptyFragmentSuffix = true;
+
+        String schemaUri = JsonSchemaFactory.normalizeMetaSchemaUri(jsonNode.get(SCHEMA_TAG).asText(), forceHttps, removeEmptyFragmentSuffix);
         if (schemaUri.equals(JsonMetaSchema.getV4().getUri()))
             return SpecVersion.VersionFlag.V4;
         else if (schemaUri.equals(JsonMetaSchema.getV6().getUri()))
diff --git a/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java b/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java
index d9ddd80..f0b26e7 100644
--- a/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java
+++ b/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java
@@ -84,9 +84,11 @@ public class JsonNodeUtil {
             }
 
             ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
-            if(JsonType.NULL.equals(nodeType) ){
-                if(state.isComplexValidator() && JsonNodeUtil.isNodeNullable(parentSchema.getParentSchema().getSchemaNode(), config) || JsonNodeUtil.isNodeNullable(parentSchema.getSchemaNode()) ){
-                    return true;
+            if(JsonType.NULL.equals(nodeType)) {
+                if(state.isComplexValidator() && parentSchema != null) {
+                    if( parentSchema.getParentSchema() != null && JsonNodeUtil.isNodeNullable(parentSchema.getParentSchema().getSchemaNode(), config) || JsonNodeUtil.isNodeNullable(parentSchema.getSchemaNode()) ) {
+                        return true;
+                    }
                 }
             }
 


Test patch:
diff --git a/src/test/java/com/networknt/schema/AdditionalPropertiesOneOfFailsTest.java b/src/test/java/com/networknt/schema/AdditionalPropertiesOneOfFailsTest.java
new file mode 100644
index 0000000..472aa28
--- /dev/null
+++ b/src/test/java/com/networknt/schema/AdditionalPropertiesOneOfFailsTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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 com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.Set;
+
+public class AdditionalPropertiesOneOfFailsTest {
+
+    private static Set<ValidationMessage> errors = null;
+
+    protected JsonSchema getJsonSchemaFromStreamContent(InputStream schemaContent) {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        return factory.getSchema(schemaContent);
+    }
+
+    protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode node = mapper.readTree(content);
+        return node;
+    }
+
+    @BeforeEach
+    public void withJsonSchema() {
+        // correct processing would include the  assertions in the tests
+        if (errors == null) {
+            String schemaPathJson = "/openapi3/AdditionalPropertiesOneOfFailsTest.json";
+            String dataPath = "/data/AdditionalPropertiesOneOfFailsTest.json";
+            InputStream schemaInputStream = getClass().getResourceAsStream(schemaPathJson);
+
+            JsonSchema schema = getJsonSchemaFromStreamContent(schemaInputStream);
+            schema.getValidationContext().getConfig().setFailFast(false);
+
+
+            InputStream dataInputStream = getClass().getResourceAsStream(dataPath);
+            try {
+                JsonNode node = getJsonNodeFromStreamContent(dataInputStream);
+
+                errors = schema.validate(node);
+
+                System.out.println("nr. of reported errors: " + errors.size());
+                errors.stream().forEach(er -> System.out.println(er.toString()));
+            } catch (Exception e) {
+                System.out.println("Fail!");
+            }
+
+        }
+    }
+
+    @Test
+    @Disabled
+    public void toxicIsAdditional() {
+        Assertions.assertTrue(errors.stream().filter(er -> er.toString().contains("toxic: is not defined in the schema")).count() == 2,
+                "property toxic is not defined on activity chemical");
+    }
+
+    @Test
+    @Disabled
+    public void chemicalCharacteristicNameIsAdditional() {
+
+
+        Assertions.assertTrue(errors.stream().filter(er -> er.toString().contains("$.activities[2].chemicalCharacteristic.name: is not defined in the schema")).count() == 1,
+                "property name is not defined in 'oneOf' the  ChemicalCharacteristic component schemas");
+    }
+
+
+    @Test
+    @Disabled
+    public void depthIsAdditional() {
+
+        Assertions.assertTrue(errors.stream().filter(er -> er.toString().contains("depth: is not defined in the schema")).count() == 1,
+                "property depth is not defined on activity machine");
+    }
+
+    @Test
+    @Disabled
+    public void chemicalCharacteristicCategoryNameIsDefined() {
+
+        Assertions.assertFalse(errors.stream().filter(er -> er.toString().contains("$.activities[0].chemicalCharacteristic.categoryName: is not defined in the schema")).count() == 1,
+                "property categoryName is defined in 'oneOf' the ChemicalCharacteristic component schemas ");
+    }
+
+    @Test
+    @Disabled
+    public void weightIsMissingOnlyOnce() {
+
+        Assertions.assertTrue(errors.stream().filter(er -> er.toString().contains("weight: is missing")).count() == 1,
+                "property weight is required on activity machine ");
+    }
+
+    @Test
+    @Disabled
+    public void heightIsNotMissingNotOnceAndNotTwice() {
+
+        Assertions.assertFalse(errors.stream().filter(er -> er.toString().contains("heigth: is missing")).count() == 1,
+                "property height is defined ");
+
+    }
+
+    @Test
+    @Disabled
+    public void heightWrongType() {
+
+        Assertions.assertTrue(errors.stream().filter(er -> er.toString().contains("heigth: number found, integer expected")).count() == 1,
+                "property height has the wrong type");
+
+    }
+}
diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java
new file mode 100644
index 0000000..ded8250
--- /dev/null
+++ b/src/test/java/com/networknt/schema/CustomUriTest.java
@@ -0,0 +1,64 @@
+package com.networknt.schema;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.ValidationMessage;
+import com.networknt.schema.uri.URIFactory;
+import com.networknt.schema.uri.URIFetcher;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class CustomUriTest {
+    @Test
+    public void customUri() throws Exception {
+        /* Given */
+        final JsonSchemaFactory factory = buildJsonSchemaFactory();
+        final JsonSchema schema = factory.getSchema(
+                "{\"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\"type\": \"object\",\"additionalProperties\": false,\"properties\": {\"customAnyOf\": {\"anyOf\": [{\"type\": \"null\"},{\"$ref\": \"custom:date\"}]},\"customOneOf\": {\"oneOf\": [{\"type\": \"null\"},{\"$ref\": \"custom:date\"}]}}}");
+        final ObjectMapper mapper = new ObjectMapper();
+        final JsonNode value = mapper.readTree("{\"customAnyOf\": null,\"customOneOf\": null}");
+
+        /* When */
+        final Set<ValidationMessage> errors = schema.validate(value);
+
+        /* Then */
+        assertThat(errors.isEmpty(), is(true));
+    }
+
+    private JsonSchemaFactory buildJsonSchemaFactory() {
+        return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909))
+                .uriFetcher(new CustomUriFetcher(), "custom").uriFactory(new CustomUriFactory(), "custom").build();
+    }
+
+    private static class CustomUriFetcher implements URIFetcher {
+        private static final String SCHEMA = "{\"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\"$id\":\"custom:date\",\"type\":\"string\",\"format\":\"date\"}";
+
+        @Override
+        public InputStream fetch(final URI uri) throws IOException {
+            return new ByteArrayInputStream(SCHEMA.getBytes(StandardCharsets.UTF_8));
+        }
+    }
+
+    private static class CustomUriFactory implements URIFactory {
+        @Override
+        public URI create(final String uri) {
+            return URI.create(uri);
+        }
+
+        @Override
+        public URI create(final URI baseURI, final String segment) {
+            return baseURI.resolve(segment);
+        }
+    }
+}
diff --git a/src/test/java/com/networknt/schema/Issue518Test.java b/src/test/java/com/networknt/schema/Issue518Test.java
new file mode 100644
index 0000000..722e95d
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue518Test.java
@@ -0,0 +1,30 @@
+package com.networknt.schema;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+
+public class Issue518Test {
+    private static final JsonMetaSchema igluMetaSchema =
+            JsonMetaSchema
+                    .builder("http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", JsonMetaSchema.getV7())
+                    .build();
+
+    private static final JsonSchemaFactory FACTORY =
+            JsonSchemaFactory
+                    .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+                    .addMetaSchema(igluMetaSchema)
+                    .forceHttps(false)
+                    .removeEmptyFragmentSuffix(false)
+                    .build();
+
+    @Test
+    public void testPreservingEmptyFragmentSuffix() {
+        String schemaPath = "/schema/issue518-v7.json";
+        InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath);
+        JsonSchema schema = FACTORY.getSchema(schemaInputStream);
+
+        Assertions.assertNotNull(schema);
+    }
+}
diff --git a/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java b/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java
index 7cbcf5e..601119d 100644
--- a/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java
+++ b/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java
@@ -59,15 +59,53 @@ public class UnknownMetaSchemaTest {
 
     @Test
     public void testNormalize() throws JsonSchemaException {
+        final boolean forceHttps = true;
+        final boolean removeEmptyFragmentSuffix = true;
+
         String uri01 = "http://json-schema.org/draft-07/schema";
         String uri02 = "http://json-schema.org/draft-07/schema#";
         String uri03 = "http://json-schema.org/draft-07/schema?key=value";
         String uri04 = "http://json-schema.org/draft-07/schema?key=value&key2=value2";
         String expected = "https://json-schema.org/draft-07/schema";
-        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri01, true));
-        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri02, true));
-        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri03, true));
-        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri04, true));
+
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri01, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri02, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri03, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri04, forceHttps, removeEmptyFragmentSuffix));
+
+    }
+
+    @Test
+    public void testNormalizeForceHttpsDisabled() throws JsonSchemaException {
+        final boolean forceHttps = false;
+        final boolean removeEmptyFragmentSuffix = true;
+
+        String uri01 = "http://json-schema.org/draft-07/schema";
+        String uri02 = "http://json-schema.org/draft-07/schema#";
+        String uri03 = "http://json-schema.org/draft-07/schema?key=value";
+        String uri04 = "http://json-schema.org/draft-07/schema?key=value&key2=value2";
+        String expected = "http://json-schema.org/draft-07/schema";
+
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri01, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri02, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri03, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri04, forceHttps, removeEmptyFragmentSuffix));
+
+    }
+
+    @Test
+    public void testNormalizeRemovingEmptyFragmentSuffixDisabled() throws JsonSchemaException {
+        final boolean forceHttps = true;
+        final boolean removeEmptyFragmentSuffix = false;
+
+        String uri01 = "http://json-schema.org/draft-07/schema#";
+        String uri02 = "http://json-schema.org/draft-07/schema?key=value#";
+        String uri03 = "http://json-schema.org/draft-07/schema?key=value&key2=value2#";
+        String expected = "https://json-schema.org/draft-07/schema#";
+
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri01, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri02, forceHttps, removeEmptyFragmentSuffix));
+        Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri03, forceHttps, removeEmptyFragmentSuffix));
 
     }
 }
diff --git a/src/test/resources/data/AdditionalPropertiesOneOfFailsTest.json b/src/test/resources/data/AdditionalPropertiesOneOfFailsTest.json
new file mode 100644
index 0000000..c0a36ee
--- /dev/null
+++ b/src/test/resources/data/AdditionalPropertiesOneOfFailsTest.json
@@ -0,0 +1,27 @@
+{
+  "locationName": "factoryLocation",
+  "activities": [
+    {
+      "activityType": "machine",
+      "age": "(additionalProperty not allowed)",
+      "height": 10.5
+    },
+    {
+      "activityType": "chemical",
+      "toxic": "(additionalProperty not allowed)",
+      "chemicalCharacteristic": {
+        "commonName": "methane",
+        "chemicalName": "CH4"
+      }
+    },
+    {
+      "activityType": "chemical",
+      "toxic": "(additionalProperty not allowed)",
+      "chemicalCharacteristic": {
+        "name": "methane",
+        "categoryName": "gasses",
+        "chemicalName": "CH4"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/draft2019-09/optional/format/date-time.json b/src/test/resources/draft2019-09/optional/format/date-time.json
index b5c8963..11af9ac 100644
--- a/src/test/resources/draft2019-09/optional/format/date-time.json
+++ b/src/test/resources/draft2019-09/optional/format/date-time.json
@@ -49,6 +49,21 @@
         "description": "only RFC3339 not all of ISO 8601 are valid",
         "data": "2013-350T01:01:01",
         "valid": false
+      },
+      {
+        "description": "an invalid date-time string without offset",
+        "data": "1963-06-19T08:30:06",
+        "valid": false
+      },
+      {
+        "description": "an invalid date-time string with only hours in offset",
+        "data": "1963-06-19T08:30:06+02",
+        "valid": false
+      },
+      {
+        "description": "an invalid date-time string without colon in offset",
+        "data": "1963-06-19T08:30:06+0200",
+        "valid": false
       }
     ]
   }
diff --git a/src/test/resources/draft4/optional/format.json b/src/test/resources/draft4/optional/format.json
index 4a1328d..6b9ab7d 100644
--- a/src/test/resources/draft4/optional/format.json
+++ b/src/test/resources/draft4/optional/format.json
@@ -11,9 +11,9 @@
         "valid": true
       },
       {
-        "description": "a valid date-time string without colon",
+        "description": "an invalid date-time string without colon",
         "data": "2018-07-27T00:00:01.000-0300",
-        "valid": true
+        "valid": false
       },
       {
         "description": "a valid date-time string",
@@ -56,14 +56,14 @@
         "valid": true
       },
       {
-        "description": "an valid date-time string",
+        "description": "an invalid date-time string",
         "data": "1963-12-30T08:30:06.283",
-        "valid": true
+        "valid": false
       },
       {
-        "description": "an valid date-time string",
+        "description": "an invalid date-time string",
         "data": "1963-12-30T08:30:06",
-        "valid": true
+        "valid": false
       },
       {
         "description": "an valid date-time string",
diff --git a/src/test/resources/draft7/optional/format/date-time.json b/src/test/resources/draft7/optional/format/date-time.json
index b5c8963..11af9ac 100644
--- a/src/test/resources/draft7/optional/format/date-time.json
+++ b/src/test/resources/draft7/optional/format/date-time.json
@@ -49,6 +49,21 @@
         "description": "only RFC3339 not all of ISO 8601 are valid",
         "data": "2013-350T01:01:01",
         "valid": false
+      },
+      {
+        "description": "an invalid date-time string without offset",
+        "data": "1963-06-19T08:30:06",
+        "valid": false
+      },
+      {
+        "description": "an invalid date-time string with only hours in offset",
+        "data": "1963-06-19T08:30:06+02",
+        "valid": false
+      },
+      {
+        "description": "an invalid date-time string without colon in offset",
+        "data": "1963-06-19T08:30:06+0200",
+        "valid": false
       }
     ]
   }
diff --git a/src/test/resources/openapi3/AdditionalPropertiesOneOfFailsTest.json b/src/test/resources/openapi3/AdditionalPropertiesOneOfFailsTest.json
new file mode 100644
index 0000000..14424f3
--- /dev/null
+++ b/src/test/resources/openapi3/AdditionalPropertiesOneOfFailsTest.json
@@ -0,0 +1,86 @@
+{
+  "type": "object",
+  "properties": {
+    "locationName": {
+      "type": "string"
+    },
+    "activities": {
+      "type": "array",
+      "items": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "activityType",
+              "weight",
+              "height"
+            ],
+            "additionalProperties": false,
+            "properties": {
+              "activityType": {
+                "enum": [
+                  "machine"
+                ]
+              },
+              "weight": {
+                "type": "integer"
+              },
+              "height": {
+                "type": "integer"
+              }
+            }
+          },
+          {
+            "type": "object",
+            "required": [
+              "activityType",
+              "chemicalCharacteristic"
+            ],
+            "additionalProperties": false,
+            "properties": {
+              "activityType": {
+                "enum": [
+                  "chemical"
+                ]
+              },
+              "chemicalCharacteristic": {
+                "oneOf": [
+                  {
+                    "type": "object",
+                    "required": [
+                      "chemicalName"
+                    ],
+                    "additionalProperties": false,
+                    "properties": {
+                      "commonName": {
+                        "type": "string"
+                      },
+                      "chemicalName": {
+                        "type": "string"
+                      }
+                    }
+                  },
+                  {
+                    "type": "object",
+                    "required": [
+                      "chemicalName"
+                    ],
+                    "additionalProperties": false,
+                    "properties": {
+                      "categoryName": {
+                        "type": "string"
+                      },
+                      "chemicalName": {
+                        "type": "string"
+                      }
+                    }
+                  }
+                ]
+              }
+            }
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/src/test/resources/schema/issue518-v7.json b/src/test/resources/schema/issue518-v7.json
new file mode 100644
index 0000000..c0d605c
--- /dev/null
+++ b/src/test/resources/schema/issue518-v7.json
@@ -0,0 +1,4 @@
+{
+  "$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
+  "type": "object"
+}