Skip to content

Commit 98a5f5f

Browse files
committed
Refactor discriminator
1 parent f48f36b commit 98a5f5f

File tree

6 files changed

+130
-29
lines changed

6 files changed

+130
-29
lines changed

src/main/java/com/networknt/schema/keyword/AnyOfValidator.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@ && canShortCircuit(executionContext)) {
117117
boolean discriminatorMatchFound = false;
118118
if (refNode != null) {
119119
// Check if there is a match
120-
String discriminatingValue = state.getMappedSchema();
121-
if (discriminatingValue != null) {
120+
String mappedSchema = state.getMappedSchema();
121+
if (mappedSchema != null) {
122122
String ref = refNode.asText();
123-
if (state.isExplicitMapping() && ref.equals(discriminatingValue)) {
123+
if (state.isExplicitMapping() && ref.equals(mappedSchema)) {
124124
// Explicit matching
125125
discriminatorMatchFound = true;
126126
state.setMatchedSchema(ref);
127-
} else if (!state.isExplicitMapping() && ref.endsWith(discriminatingValue)) {
127+
} else if (!state.isExplicitMapping() && ref.endsWith(mappedSchema)) {
128128
// Implicit matching
129129
discriminatorMatchFound = true;
130130
state.setMatchedSchema(ref);
@@ -180,11 +180,13 @@ && canShortCircuit(executionContext)) {
180180
* string values, but tooling MAY convert response values to strings for
181181
* comparison.
182182
*
183-
* https://spec.openapis.org/oas/v3.1.0#discriminator-object
183+
* https://spec.openapis.org/oas/v3.1.2#examples-0
184184
*/
185185
DiscriminatorState state = executionContext.getDiscriminatorMapping().get(instanceLocation);
186-
if (state != null && state.getMatchedSchema() == null && state.getDiscriminatingValue() != null) {
187-
// state.getPropertyValue() == null means there is no value at propertyName
186+
if (state != null && !state.hasMatchedSchema() && state.hasDiscriminatingValue()) {
187+
// The check for state.hasDiscriminatingValue is due to issue 988
188+
// Note that this is related to the DiscriminatorValidator by default not generating an assertion
189+
// if the discriminatingValue is not set in the payload
188190
existingErrors.add(error().keyword("discriminator").instanceNode(node)
189191
.instanceLocation(instanceLocation)
190192
.locale(executionContext.getExecutionConfig().getLocale())

src/main/java/com/networknt/schema/keyword/DiscriminatorState.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright (c) 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.networknt.schema.keyword;
218

319
/**
@@ -48,6 +64,17 @@ public void setDiscriminatingValue(String discriminatingValue) {
4864
this.discriminatingValue = discriminatingValue;
4965
}
5066

67+
/**
68+
* Returns ture if the discriminating value is found in the payload
69+
* corresponding to the property name.
70+
*
71+
* @return true if the discriminating value is found in the payload
72+
* corresponding to the property name
73+
*/
74+
public boolean hasDiscriminatingValue() {
75+
return this.discriminatingValue != null;
76+
}
77+
5178
/**
5279
* Gets the mapped schema which is the discriminating value mapped to the schema
5380
* name or uri.
@@ -68,19 +95,59 @@ public void setMappedSchema(String mappedSchema) {
6895
this.mappedSchema = mappedSchema;
6996
}
7097

98+
/**
99+
* Gets whether the mapping is explicit using the mappings on the discriminator
100+
* keyword.
101+
*
102+
* @return true if the mapping is explicitly mapped using mappings
103+
*/
71104
public boolean isExplicitMapping() {
72105
return explicitMapping;
73106
}
74107

108+
/**
109+
* Sets whether the mapping is explicit using the mappings on the discriminator
110+
* keyword.
111+
*
112+
* @param explicitMapping true if explicitly mapped using mappings
113+
*/
75114
public void setExplicitMapping(boolean explicitMapping) {
76115
this.explicitMapping = explicitMapping;
77116
}
78117

118+
/**
119+
* Sets the matched schema $ref.
120+
*
121+
* @param matchedSchema the matched schema $ref
122+
*/
79123
public void setMatchedSchema(String matchedSchema) {
80124
this.matchedSchema = matchedSchema;
81125
}
82126

127+
/**
128+
* Gets the matched schema $ref.
129+
*
130+
* @return the matched schema $ref
131+
*/
83132
public String getMatchedSchema() {
84133
return this.matchedSchema;
85134
}
135+
136+
/**
137+
* Returns true if there was a schema that matched the discriminating value.
138+
* <p>
139+
* If the discriminating value does not match an implicit or explicit mapping,
140+
* no schema can be determined and validation SHOULD fail. Therefore if this
141+
* returns false validation should fail.
142+
* <p>
143+
* <a href="https://spec.openapis.org/oas/v3.1.2#examples-0">4.8.25.4
144+
* Examples</a>
145+
*
146+
* @return true if there was a schema that matched the discriminating value.
147+
*/
148+
public boolean hasMatchedSchema() {
149+
return this.matchedSchema != null;
150+
}
151+
152+
86153
}

src/main/java/com/networknt/schema/keyword/DiscriminatorValidator.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@
3333
import com.networknt.schema.SchemaContext;
3434

3535
/**
36-
* {@link KeywordValidator} that resolves discriminator.
36+
* {@link KeywordValidator} for discriminator.
37+
* <p>
38+
* Note that discriminator MUST NOT change the validation outcome of the schema.
39+
* <p>
40+
* <a href=
41+
* "https://spec.openapis.org/oas/v3.1.2#discriminator-object">Discriminator
42+
* Object</>
3743
*/
3844
public class DiscriminatorValidator extends BaseKeywordValidator {
3945
/**
@@ -83,19 +89,31 @@ public void validate(ExecutionContext executionContext, JsonNode node, JsonNode
8389
DiscriminatorState state = null;
8490
DiscriminatorState existing = executionContext.getDiscriminatorMapping().get(instanceLocation);
8591
if (existing != null) {
92+
/*
93+
* By default this does not throw an exception unless strict for discriminator
94+
* is set to true.
95+
*
96+
* This default is in line with the fact that the discriminator keyword doesn't
97+
* affect validation but just helps to filter the messages.
98+
*/
99+
if (this.schemaContext.getSchemaRegistryConfig().isStrict("discriminator", Boolean.FALSE)) {
100+
throw new SchemaException("Schema at " + this.schemaLocation
101+
+ " has a discriminator keyword for which another discriminator keyword has already been set for at "
102+
+ instanceLocation);
103+
}
86104
/*
87105
* This allows a new discriminator keyword if the propertyName is empty or if
88106
* the propertyName value is the same as the existing one.
89107
*
90-
* This is not specification compliant. There shouldn't be multiple matching
91-
* discriminator keywords for the same instance.
108+
* In the specification the behavior of this is undefined. There shouldn't be
109+
* multiple matching discriminator keywords for the same instance.
92110
*
93111
* Also propertyName for the discriminator keyword should not be empty according
94112
* to the specification.
95113
*/
96114
if (!"".equals(this.propertyName) && !existing.getPropertyName().equals(this.propertyName)) {
97115
throw new SchemaException("Schema at " + this.schemaLocation
98-
+ " is redefining the discriminator property that has already been set for "
116+
+ " is redefining the discriminator property that has already been set for at "
99117
+ instanceLocation);
100118
}
101119
state = existing;
@@ -126,16 +144,26 @@ public void validate(ExecutionContext executionContext, JsonNode node, JsonNode
126144
state.setMappedSchema(mappedSchema);
127145
state.setExplicitMapping(true);
128146
} else {
129-
// If explicit mapping not found use implicit value
130-
state.setMappedSchema(discriminatingValue);
131-
state.setExplicitMapping(false);
147+
if (!state.isExplicitMapping()) { // only sets implicit if an explicit mapping was not previously set
148+
// If explicit mapping not found use implicit value
149+
state.setMappedSchema(discriminatingValue);
150+
state.setExplicitMapping(false);
151+
}
132152
}
133153
} else {
134154
/*
135155
* The property is not present in the payload. This property SHOULD be required
136156
* in the payload schema, as the behavior when the property is absent is
137157
* undefined.
138158
*/
159+
/*
160+
* By default this does not generate an assertion unless strict for
161+
* discriminator is set to true.
162+
*
163+
* This default is in line with the intent that discriminator should be an
164+
* annotation and not an assertion and shouldn't change the result which was why
165+
* the specification changed from MUST to SHOULD.
166+
*/
139167
if (this.schemaContext.getSchemaRegistryConfig().isStrict("discriminator", Boolean.FALSE)) {
140168
executionContext.addError(error().instanceNode(node).instanceLocation(instanceLocation)
141169
.messageKey("discriminator.missing_discriminating_value").arguments(this.propertyName).build());

src/main/resources/jsv-messages.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,5 @@ uniqueItems = must have only unique items in the array
6868
writeOnly = is a write-only field, it cannot appear in the data
6969
contentEncoding = does not match content encoding {0}
7070
contentMediaType = is not a content media type
71-
discriminator.missing_discriminating_value = required property ''{0}'' for discriminator not found
71+
discriminator.missing_discriminating_value = required property ''{0}'' for discriminator not found
72+
discriminator.anyOf.

src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,10 @@ void anyOfRedefinedDiscriminatorAndDiscriminatorWithMissingPropertyName() {
969969
Schema schema = factory.getSchema(schemaData);
970970
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
971971
List<Error> list = messages.stream().collect(Collectors.toList());
972+
// There should be no errors as discriminator should not affect the validation result of anyOf
973+
// Although the matched schema has the following error
972974
// : required property 'numberOfBeds' not found
975+
// There is still a schema in the anyOf that matches
973976
assertTrue(list.isEmpty());
974977
}
975978

src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@ private void runTestFile(String testCaseFile) throws Exception {
3030
try {
3131
JsonNode testCase = testCases.get(j);
3232
System.out.println("Test Case ["+(j+1)+"]: "+testCase.get("description"));
33-
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(testCase.get("schema")));
34-
System.out.println("Tests:");
33+
//System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(testCase.get("schema")));
3534
ArrayNode testNodes = (ArrayNode) testCase.get("tests");
3635
for (int i = 0; i < testNodes.size(); i++) {
3736
JsonNode test = testNodes.get(i);
38-
System.out.println(test);
37+
System.out.println("> Test Data ["+(i+1)+"]: "+test);
3938
JsonNode node = test.get("data");
4039
JsonNode typeLooseNode = test.get("isTypeLoose");
4140
// Configure the schemaValidator to set typeLoose's value based on the test file,
@@ -51,10 +50,10 @@ private void runTestFile(String testCaseFile) throws Exception {
5150

5251
if (test.get("valid").asBoolean()) {
5352
if (!errors.isEmpty()) {
54-
System.out.println("---- test case failed ----");
55-
System.out.println("schema: " + schema);
56-
System.out.println("data: " + test.get("data"));
57-
System.out.println("errors:");
53+
System.out.println("---- Test Data ["+(i+1)+"] FAILED [Unexpected Errors] ----");
54+
System.out.println("> Schema: " + schema);
55+
System.out.println("> Data : " + test.get("data"));
56+
System.out.println("> Errors:");
5857
for (Error error : errors) {
5958
System.out.println(error);
6059
}
@@ -63,16 +62,17 @@ private void runTestFile(String testCaseFile) throws Exception {
6362
} else {
6463
// System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(testCase.get("schema")));
6564
if (errors.isEmpty()) {
66-
System.out.println("---- test case failed ----");
67-
System.out.println("schema: " + schema);
68-
System.out.println("data: " + test.get("data"));
65+
System.out.println("---- Test Data ["+(i+1)+"] FAILED [Unexpected Success] ----");
66+
System.out.println("> Schema: " + schema);
67+
System.out.println("> Data : " + test.get("data"));
6968
} else {
7069
JsonNode errorCount = test.get("errorCount");
7170
if (errorCount != null && errorCount.isInt() && errors.size() != errorCount.asInt()) {
72-
System.out.println("---- test case failed ----");
73-
System.out.println("schema: " + schema);
74-
System.out.println("data: " + test.get("data"));
75-
System.out.println("errors: " + errors);
71+
System.out.println("---- Test Data [" + (i + 1) + "] FAILED [Expected "
72+
+ errorCount.asInt() + " Errors but was " + errors.size() + "] ----");
73+
System.out.println("> Schema: " + schema);
74+
System.out.println("> Data : " + test.get("data"));
75+
System.out.println("> Errors: " + errors);
7676
for (Error error : errors) {
7777
System.out.println(error);
7878
}

0 commit comments

Comments
 (0)