Repository: networknt/json-schema-validator
Pull request #1078 (2024-07-01 11:39:15)

Fix oneOf when discriminator enabled but no discriminator
Fixes issue when oneOf when discriminator enabled but no discriminator causes no details in errors. Such cases should behave as a normal oneOf.

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1078?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 77.98%. Comparing base [(`48ca3c2`)](https://app.codecov.io/gh/networknt/json-schema-validator/commit/48ca3c22995e32797e9defdb8b77869f0d15caf3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) to head [(`ebb2b65`)](https://app.codecov.io/gh/networknt/json-schema-validator/commit/ebb2b65d5882c12a2f515cf333d2574f57c293df?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).
> Report is 42 commits behind head on master.

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##             master    #1078      +/-   ##
============================================
- Coverage     78.90%   77.98%   -0.92%     
- Complexity     1965     2053      +88     
============================================
  Files           172      198      +26     
  Lines          6352     6855     +503     
  Branches       1255     1257       +2     
============================================
+ Hits           5012     5346     +334     
- Misses          867     1032     +165     
- Partials        473      477       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1078?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).


Patch:
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index d964def..1807305 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -16,13 +16,19 @@
 
 package com.networknt.schema;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.utils.SetView;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.*;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.networknt.schema.utils.SetView;
 
 /**
  * {@link JsonValidator} for oneOf.
@@ -75,10 +81,15 @@ public class OneOfValidator extends BaseJsonValidator {
             if (this.validationContext.getConfig().isDiscriminatorKeywordEnabled()) {
                 DiscriminatorContext discriminatorContext = new DiscriminatorContext();
                 executionContext.enterDiscriminatorContext(discriminatorContext, instanceLocation);
-                
+
                 // check if discriminator present
                 discriminator = (DiscriminatorValidator) this.getParentSchema().getValidators().stream()
                         .filter(v -> "discriminator".equals(v.getKeyword())).findFirst().orElse(null);
+                if (discriminator != null) {
+                    // this is just to make the discriminator context active
+                    discriminatorContext.registerDiscriminator(discriminator.getSchemaLocation(),
+                            (ObjectNode) discriminator.getSchemaNode());
+                }
             }
             executionContext.setFailFast(false);
             for (JsonSchema schema : this.schemas) {
@@ -135,7 +146,8 @@ public class OneOfValidator extends BaseJsonValidator {
                         // found is null triggers on the correct schema
                         childErrors = new SetView<>();
                         childErrors.union(schemaErrors);
-                    } else if (currentDiscriminatorContext.isDiscriminatorIgnore()) {
+                    } else if (currentDiscriminatorContext.isDiscriminatorIgnore()
+                            || !currentDiscriminatorContext.isActive()) {
                         // This is the normal handling when discriminators aren't enabled
                         if (childErrors == null) {
                             childErrors = new SetView<>();
diff --git a/src/main/java/com/networknt/schema/utils/JsonNodes.java b/src/main/java/com/networknt/schema/utils/JsonNodes.java
index f1f8947..3bfd9aa 100644
--- a/src/main/java/com/networknt/schema/utils/JsonNodes.java
+++ b/src/main/java/com/networknt/schema/utils/JsonNodes.java
@@ -67,7 +67,7 @@ public class JsonNodes {
     @SuppressWarnings("unchecked")
     public static <T extends JsonNode> T get(JsonNode node, Object propertyOrIndex) {
         JsonNode value = null;
-        if (propertyOrIndex instanceof Number) {
+        if (propertyOrIndex instanceof Number && node.isArray()) {
             value = node.get(((Number) propertyOrIndex).intValue());
         } else {
             value = node.get(propertyOrIndex.toString());


Test patch:
diff --git a/src/test/java/com/networknt/schema/OneOfValidatorTest.java b/src/test/java/com/networknt/schema/OneOfValidatorTest.java
index 70b4f13..4fdb14a 100644
--- a/src/test/java/com/networknt/schema/OneOfValidatorTest.java
+++ b/src/test/java/com/networknt/schema/OneOfValidatorTest.java
@@ -284,4 +284,189 @@ public class OneOfValidatorTest {
                 + "}";
         assertFalse(schema.validate(example3, InputFormat.JSON, OutputFormat.BOOLEAN));
     }
+
+    /**
+     * Test for when the discriminator keyword is enabled but no discriminator is
+     * present in the schema. This should process as a normal oneOf and return the
+     * error messages.
+     */
+    @Test
+    void oneOfDiscriminatorEnabled() {
+        String schemaData = "{\r\n"
+                + "  \"oneOf\": [\r\n"
+                + "    {\r\n"
+                + "      \"type\": \"string\"\r\n"
+                + "    },\r\n"
+                + "    {\r\n"
+                + "      \"type\": \"number\"\r\n"
+                + "    }\r\n"
+                + "  ]\r\n"
+                + "}";
+        JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData,
+                SchemaValidatorsConfig.builder().discriminatorKeywordEnabled(true).build());
+        String inputData = "{}";
+        Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
+        assertEquals(3, messages.size());
+    }
+
+    /**
+     * Standard case where the discriminator is in the same schema as oneOf.
+     * <p>
+     * Note that discriminators do not affect the validation result and can only
+     * affect the messages returned.
+     */
+    @Test
+    void oneOfDiscriminatorEnabledWithDiscriminator() {
+        String schemaData = "{\r\n"
+                + "  \"discriminator\": {\r\n"
+                + "    \"propertyName\": \"type\",\r\n"
+                + "    \"mapping\": {\r\n"
+                + "      \"string\": \"#/$defs/string\",\r\n"
+                + "      \"number\": \"#/$defs/number\"\r\n"
+                + "    }\r\n"
+                + "  },\r\n"
+                + "  \"oneOf\": [\r\n"
+                + "    {\r\n"
+                + "      \"$ref\": \"#/$defs/string\"\r\n"
+                + "    },\r\n"
+                + "    {\r\n"
+                + "      \"$ref\": \"#/$defs/number\"\r\n"
+                + "    }\r\n"
+                + "  ],\r\n"
+                + "  \"$defs\": {\r\n"
+                + "    \"string\": {\r\n"
+                + "      \"properties\": {\r\n"
+                + "        \"type\": {\r\n"
+                + "          \"type\": \"string\"\r\n"
+                + "        },\r\n"
+                + "        \"value\": {\r\n"
+                + "          \"type\": \"string\"\r\n"
+                + "        }\r\n"
+                + "      }\r\n"
+                + "    },\r\n"
+                + "    \"number\": {\r\n"
+                + "      \"properties\": {\r\n"
+                + "        \"type\": {\r\n"
+                + "          \"type\": \"string\"\r\n"
+                + "        },\r\n"
+                + "        \"value\": {\r\n"
+                + "          \"type\": \"number\"\r\n"
+                + "        }\r\n"
+                + "      }\r\n"
+                + "    }\r\n"
+                + "  }\r\n"
+                + "}";
+        JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData,
+                SchemaValidatorsConfig.builder().discriminatorKeywordEnabled(true).build());
+        // Valid
+        String inputData = "{\r\n"
+                + "  \"type\": \"number\",\r\n"
+                + "  \"value\": 1\r\n"
+                + "}";
+        Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
+        assertEquals(0, messages.size());
+
+        // Invalid only 1 message returned for number
+        String inputData2 = "{\r\n"
+                + "  \"type\": \"number\",\r\n"
+                + "  \"value\": {}\r\n"
+                + "}";
+        Set<ValidationMessage> messages2 = schema.validate(inputData2, InputFormat.JSON);
+        assertEquals(2, messages2.size());
+
+        // Invalid both messages for string and object returned
+        JsonSchema schema2 = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData,
+                SchemaValidatorsConfig.builder().discriminatorKeywordEnabled(false).build());
+        Set<ValidationMessage> messages3 = schema2.validate(inputData2, InputFormat.JSON);
+        assertEquals(3, messages3.size());
+    }
+
+    /**
+     * Subclass case where the discriminator is in an allOf inside one of the oneOf references.
+     * <p>
+     * Note that discriminators do not affect the validation result and can only
+     * affect the messages returned.
+     */
+    @Test
+    void oneOfDiscriminatorEnabledWithDiscriminatorInSubclass() {
+        String schemaData = "{\r\n"
+                + "  \"oneOf\": [\r\n"
+                + "    {\r\n"
+                + "      \"$ref\": \"#/$defs/string\"\r\n"
+                + "    },\r\n"
+                + "    {\r\n"
+                + "      \"$ref\": \"#/$defs/number\"\r\n"
+                + "    }\r\n"
+                + "  ],\r\n"
+                + "  \"$defs\": {\r\n"
+                + "    \"typed\": {\r\n"
+                + "      \"discriminator\": {\r\n"
+                + "        \"propertyName\": \"type\",\r\n"
+                + "        \"mapping\": {\r\n"
+                + "          \"string\": \"#/$defs/string\",\r\n"
+                + "          \"number\": \"#/$defs/number\"\r\n"
+                + "        }\r\n"
+                + "      }\r\n"
+                + "    },\r\n"
+                + "    \"string\": {\r\n"
+                + "      \"allOf\": [\r\n"
+                + "        {\r\n"
+                + "          \"$ref\": \"#/$defs/typed\"\r\n"
+                + "        },\r\n"
+                + "        {\r\n"
+                + "          \"properties\": {\r\n"
+                + "            \"type\": {\r\n"
+                + "              \"type\": \"string\"\r\n"
+                + "            },\r\n"
+                + "            \"value\": {\r\n"
+                + "              \"type\": \"string\"\r\n"
+                + "            }\r\n"
+                + "          }\r\n"
+                + "        }\r\n"
+                + "      ]\r\n"
+                + "    },\r\n"
+                + "    \"number\": {\r\n"
+                + "      \"allOf\": [\r\n"
+                + "        {\r\n"
+                + "          \"$ref\": \"#/$defs/typed\"\r\n"
+                + "        },\r\n"
+                + "        {\r\n"
+                + "          \"properties\": {\r\n"
+                + "            \"type\": {\r\n"
+                + "              \"type\": \"string\"\r\n"
+                + "            },\r\n"
+                + "            \"value\": {\r\n"
+                + "              \"type\": \"number\"\r\n"
+                + "            }\r\n"
+                + "          }\r\n"
+                + "        }\r\n"
+                + "      ]\r\n"
+                + "    }\r\n"
+                + "  }\r\n"
+                + "}";
+        JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData,
+                SchemaValidatorsConfig.builder().discriminatorKeywordEnabled(true).build());
+        // Valid
+        String inputData = "{\r\n"
+                + "  \"type\": \"number\",\r\n"
+                + "  \"value\": 1\r\n"
+                + "}";
+        Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
+        assertEquals(0, messages.size());
+
+        // Invalid only 1 message returned for number
+        String inputData2 = "{\r\n"
+                + "  \"type\": \"number\",\r\n"
+                + "  \"value\": {}\r\n"
+                + "}";
+        Set<ValidationMessage> messages2 = schema.validate(inputData2, InputFormat.JSON);
+        assertEquals(2, messages2.size());
+
+        // Invalid both messages for string and object returned
+        JsonSchema schema2 = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData,
+                SchemaValidatorsConfig.builder().discriminatorKeywordEnabled(false).build());
+        Set<ValidationMessage> messages3 = schema2.validate(inputData2, InputFormat.JSON);
+        assertEquals(3, messages3.size());
+    }
+
 }
diff --git a/src/test/java/com/networknt/schema/oas/OpenApi30Test.java b/src/test/java/com/networknt/schema/oas/OpenApi30Test.java
index 43256d2..e00abb4 100644
--- a/src/test/java/com/networknt/schema/oas/OpenApi30Test.java
+++ b/src/test/java/com/networknt/schema/oas/OpenApi30Test.java
@@ -16,6 +16,7 @@
 package com.networknt.schema.oas;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import java.util.List;
 import java.util.Set;
@@ -64,4 +65,16 @@ class OpenApi30Test {
         assertEquals("required", list.get(1).getType());
         assertEquals("bark", list.get(1).getProperty());
     }
+
+    /**
+     * Tests that schema location with number in fragment can resolve.
+     */
+    @Test
+    void jsonPointerWithNumberInFragment() {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V7, builder -> builder
+                .metaSchema(OpenApi30.getInstance()).defaultMetaSchemaIri(OpenApi30.getInstance().getIri()));
+        JsonSchema schema = factory.getSchema(SchemaLocation.of(
+                "classpath:schema/oas/3.0/petstore.yaml#/paths/~1pet/post/responses/200/content/application~1json/schema"));
+        assertNotNull(schema);
+    }
 }