Skip to content

Commit bd085ed

Browse files
authored
Fixes for discriminator (#971)
* Fix discriminator * Fix oneOf * Filter messages for oneOf discriminator * Support discriminator in oneOf sibling * Fix * Fix * Fix * Support mappings * Refactor * Update doc
1 parent 2879ca3 commit bd085ed

File tree

7 files changed

+808
-28
lines changed

7 files changed

+808
-28
lines changed

doc/openapi-discriminators.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,32 @@
22

33
## OpenAPI 3.x discriminator support
44

5-
Starting with `1.0.51`, `json-schema-validator` partly supports the use of discriminators as described under
6-
https://github.com/OAI/OpenAPI-Specification/blame/master/versions/3.0.3.md#L2693 and following.
5+
Starting with `1.0.51`, `json-schema-validator` partly supports the use of the [`discriminator`](https://github.com/OAI/OpenAPI-Specification/blob/7cc8f4c4e742a20687fa65ace54ed32fcb8c6df0/versions/3.1.0.md#discriminator-object) keyword.
6+
7+
Note that the use of the `discriminator` keyword does not affect the validation of `anyOf` or `oneOf`. The use of `discriminator` is not equivalent to having a `if`/`then` with the `discriminator` propertyName.
8+
9+
When a `discriminator` is used, the assertions generated by `anyOf` or `oneOf` will only be the assertions generated from the schema that the discriminator applies to. An assertion will be generated if a `discriminator` is used but there is no matching schema that maps to the value in the `propertyName`.
710

811
## How to use
912

1013
1. Configure `SchemaValidatorsConfig` accordingly:
1114
```java
1215
class Demo{
13-
void demo() {
16+
void demo() {
1417
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
1518
config.setOpenAPI3StyleDiscriminators(true); // defaults to false
16-
}
19+
}
1720
}
1821
```
19-
2. Use the configured `SchemaValidatorsConfig` with the `JSONSchemaFactory` when creating the `JSONSchema`
22+
2. Use the configured `SchemaValidatorsConfig` with the `JsonSchemaFactory` when creating the `JsonSchema`
2023
```java
2124
class Demo{
22-
void demo() {
25+
void demo() {
2326
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
2427
config.setOpenAPI3StyleDiscriminators(true); // defaults to false
25-
JsonSchema schema = validatorFactory.getSchema(schemaURI, schemaJacksonJsonNode, config);
26-
}
28+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
29+
JsonSchema schema = factory.getSchema(schemaURI, schemaJacksonJsonNode, config);
30+
}
2731
}
2832
```
2933
3. Ensure that the type field that you want to use as discriminator `propertyName` is required in your schema
@@ -43,9 +47,6 @@ those parts that are indisputable are considered at this moment.
4347

4448
* `propertyName` redefinition is prohibited on additive discriminators
4549
* `mapping` key redefinition is also prohibited on additive discriminators
46-
* `oneOf` ignores discriminators as today it is not clear from the spec whether `oneOf` + `discriminator` should be equal to
47-
`anyOf` + `discriminator` or not. Especially if `oneOf` should respect the discriminator and skip the other schemas, it's
48-
functionally not JSON Schema `oneOf` anymore as multiple matches would not make the validation fail anymore.
4950
* the specification indicates that inline properties should be ignored.
5051
So, this example would respect `foo`
5152
```yaml
@@ -71,11 +72,11 @@ those parts that are indisputable are considered at this moment.
7172

7273
## Schema Examples
7374

74-
more examples in https://github.com/networknt/json-schema-validator/blob/master/src/test/resources/openapi3/discriminator.json
75+
More examples in https://github.com/networknt/json-schema-validator/blob/master/src/test/resources/openapi3/discriminator.json
7576

7677
### Base type and extended type (the `anyOf` forward references are required)
7778

78-
#### the simplest example:
79+
#### Example:
7980

8081
```json
8182
{

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public class AnyOfValidator extends BaseJsonValidator {
3232
private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation";
3333

3434
private final List<JsonSchema> schemas = new ArrayList<>();
35-
private final DiscriminatorContext discriminatorContext;
3635

3736
private Boolean canShortCircuit = null;
3837

@@ -43,12 +42,6 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath
4342
this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i),
4443
schemaNode.get(i), parentSchema));
4544
}
46-
47-
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
48-
this.discriminatorContext = new DiscriminatorContext();
49-
} else {
50-
this.discriminatorContext = null;
51-
}
5245
}
5346

5447
@Override
@@ -59,7 +52,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
5952
ValidatorState state = executionContext.getValidatorState();
6053

6154
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
62-
executionContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation);
55+
executionContext.enterDiscriminatorContext(new DiscriminatorContext(), instanceLocation);
6356
}
6457

6558
boolean initialHasMatchedNode = state.hasMatchedNode();
@@ -113,7 +106,7 @@ && canShortCircuit() && canShortCircuit(executionContext)) {
113106
// return empty errors.
114107
return errors;
115108
} else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
116-
if (this.discriminatorContext.isDiscriminatorMatchFound()) {
109+
if (executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound()) {
117110
if (!errors.isEmpty()) {
118111
// The following is to match the previous logic adding to all errors
119112
// which is generally discarded as it returns errors but the allErrors
@@ -143,7 +136,8 @@ && canShortCircuit() && canShortCircuit(executionContext)) {
143136
executionContext.setFailFast(failFast);
144137
}
145138

146-
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) {
139+
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()
140+
&& executionContext.getCurrentDiscriminatorContext().isActive()) {
147141
return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
148142
.locale(executionContext.getExecutionConfig().getLocale())
149143
.arguments(
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2024 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+
17+
package com.networknt.schema;
18+
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.Iterator;
22+
import java.util.Map;
23+
import java.util.Map.Entry;
24+
import java.util.Set;
25+
26+
import com.fasterxml.jackson.databind.JsonNode;
27+
import com.fasterxml.jackson.databind.node.ObjectNode;
28+
29+
/**
30+
* {@link JsonValidator} that resolves discriminator.
31+
*/
32+
public class DiscriminatorValidator extends BaseJsonValidator {
33+
private final String propertyName;
34+
private final Map<String, String> mapping;
35+
36+
public DiscriminatorValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
37+
JsonSchema parentSchema, ValidationContext validationContext) {
38+
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DISCRIMINATOR,
39+
validationContext);
40+
ObjectNode discriminator = schemaNode.isObject() ? (ObjectNode) schemaNode : null;
41+
if (discriminator != null) {
42+
JsonNode propertyName = discriminator.get("propertyName");
43+
this.propertyName = propertyName != null ? propertyName.asText() : "";
44+
JsonNode mappingNode = discriminator.get("mapping");
45+
ObjectNode mapping = mappingNode != null && mappingNode.isObject() ? (ObjectNode) mappingNode : null;
46+
if (mapping != null) {
47+
this.mapping = new HashMap<>();
48+
for (Iterator<Entry<String, JsonNode>> iter = mapping.fields(); iter.hasNext();) {
49+
Entry<String, JsonNode> entry = iter.next();
50+
this.mapping.put(entry.getKey(), entry.getValue().asText());
51+
}
52+
} else {
53+
this.mapping = Collections.emptyMap();
54+
}
55+
} else {
56+
this.propertyName = "";
57+
this.mapping = Collections.emptyMap();
58+
}
59+
}
60+
61+
@Override
62+
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
63+
JsonNodePath instanceLocation) {
64+
return Collections.emptySet();
65+
}
66+
67+
/**
68+
* Gets the property name of the discriminator.
69+
*
70+
* @return the property name
71+
*/
72+
public String getPropertyName() {
73+
return propertyName;
74+
}
75+
76+
/**
77+
* Gets the mapping to map the property name value to the schema name.
78+
*
79+
* @return the discriminator mappings
80+
*/
81+
public Map<String, String> getMapping() {
82+
return mapping;
83+
}
84+
}

src/main/java/com/networknt/schema/JsonMetaSchema.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,14 @@ public JsonValidator newValidator(ValidationContext validationContext, SchemaLoc
358358
try {
359359
Keyword kw = this.keywords.get(keyword);
360360
if (kw == null) {
361+
if ("message".equals(keyword) && validationContext.getConfig().isCustomMessageSupported()) {
362+
return null;
363+
}
364+
if (ValidatorTypeCode.DISCRIMINATOR.getValue().equals(keyword)
365+
&& validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
366+
return ValidatorTypeCode.DISCRIMINATOR.newValidator(schemaLocation, evaluationPath, schemaNode,
367+
parentSchema, validationContext);
368+
}
361369
if (UNKNOWN_KEYWORDS.put(keyword, keyword) == null) {
362370
logger.warn("Unknown keyword {} - you should define your own Meta Schema. If the keyword is irrelevant for validation, just use a NonValidationKeyword", keyword);
363371
}

src/main/java/com/networknt/schema/OneOfValidator.java

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
6262
// Save flag as nested schema evaluation shouldn't trigger fail fast
6363
boolean failFast = executionContext.isFailFast();
6464
try {
65+
DiscriminatorValidator discriminator = null;
66+
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
67+
DiscriminatorContext discriminatorContext = new DiscriminatorContext();
68+
executionContext.enterDiscriminatorContext(discriminatorContext, instanceLocation);
69+
70+
// check if discriminator present
71+
discriminator = (DiscriminatorValidator) this.getParentSchema().getValidators().stream()
72+
.filter(v -> "discriminator".equals(v.getKeyword())).findFirst().orElse(null);
73+
}
6574
executionContext.setFailFast(false);
6675
for (JsonSchema schema : this.schemas) {
6776
Set<ValidationMessage> schemaErrors = Collections.emptySet();
@@ -95,23 +104,62 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
95104
// note that the short circuit means that only 2 valid schemas are reported even if could be more
96105
break;
97106
}
98-
99-
if (!schemaErrors.isEmpty() && reportChildErrors(executionContext)) {
107+
108+
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
109+
// The discriminator will cause all messages other than the one with the
110+
// matching discriminator to be discarded. Note that the discriminator cannot
111+
// affect the actual validation result.
112+
if (discriminator != null && !discriminator.getPropertyName().isEmpty()) {
113+
String discriminatorPropertyValue = node.get(discriminator.getPropertyName()).asText();
114+
discriminatorPropertyValue = discriminator.getMapping().getOrDefault(discriminatorPropertyValue,
115+
discriminatorPropertyValue);
116+
JsonNode refNode = schema.getSchemaNode().get("$ref");
117+
if (refNode != null) {
118+
String ref = refNode.asText();
119+
if (ref.equals(discriminatorPropertyValue) || ref.endsWith("/" + discriminatorPropertyValue)) {
120+
executionContext.getCurrentDiscriminatorContext().markMatch();
121+
}
122+
}
123+
}
124+
boolean discriminatorMatchFound = executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound();
125+
if (discriminatorMatchFound && childErrors == null) {
126+
// Note that the match is set if found and not reset so checking if childErrors
127+
// found is null triggers on the correct schema
128+
childErrors = new SetView<>();
129+
childErrors.union(schemaErrors);
130+
}
131+
} else if (!schemaErrors.isEmpty() && reportChildErrors(executionContext)) {
132+
// This is the normal handling when discriminators aren't enabled
100133
if (childErrors == null) {
101134
childErrors = new SetView<>();
102135
}
103136
childErrors.union(schemaErrors);
104137
}
105138
index++;
106139
}
140+
141+
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()
142+
&& (discriminator != null || executionContext.getCurrentDiscriminatorContext().isActive())
143+
&& !executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound()) {
144+
errors = Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
145+
.locale(executionContext.getExecutionConfig().getLocale())
146+
.arguments(
147+
"based on the provided discriminator. No alternative could be chosen based on the discriminator property")
148+
.build());
149+
}
107150
} finally {
108151
// Restore flag
109152
executionContext.setFailFast(failFast);
153+
154+
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
155+
executionContext.leaveDiscriminatorContextImmediately(instanceLocation);
156+
}
110157
}
111158

112159
// ensure there is always an "OneOf" error reported if number of valid schemas
113160
// is not equal to 1.
114-
if (numberOfValidSchema != 1) {
161+
// errors will only not be null in the discriminator case where no match is found
162+
if (numberOfValidSchema != 1 && errors == null) {
115163
ValidationMessage message = message().instanceNode(node).instanceLocation(instanceLocation)
116164
.messageKey(numberOfValidSchema > 1 ? "oneOf.indexes" : "oneOf")
117165
.locale(executionContext.getExecutionConfig().getLocale())

src/main/java/com/networknt/schema/ValidatorTypeCode.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ JsonValidator newInstance(SchemaLocation schemaLocation, JsonNodePath evaluation
3232
}
3333

3434
enum VersionCode {
35+
None(new SpecVersion.VersionFlag[] { }),
3536
AllVersions(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V4, SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
3637
MinV6(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
3738
MinV7(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
@@ -51,8 +52,8 @@ enum VersionCode {
5152
}
5253

5354
EnumSet<VersionFlag> getVersions() {
54-
return this.versions;
55-
}
55+
return this.versions;
56+
}
5657
}
5758

5859
public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
@@ -66,6 +67,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
6667
DEPENDENCIES("dependencies", "1007", DependenciesValidator::new, VersionCode.AllVersions),
6768
DEPENDENT_REQUIRED("dependentRequired", "1045", DependentRequired::new, VersionCode.MinV201909),
6869
DEPENDENT_SCHEMAS("dependentSchemas", "1046", DependentSchemas::new, VersionCode.MinV201909),
70+
DISCRIMINATOR("discriminator", "2001", DiscriminatorValidator::new, VersionCode.None),
6971
DYNAMIC_REF("$dynamicRef", "1051", DynamicRefValidator::new, VersionCode.MinV202012),
7072
ENUM("enum", "1008", EnumValidator::new, VersionCode.AllVersions),
7173
EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", ExclusiveMaximumValidator::new, VersionCode.MinV6),

0 commit comments

Comments
 (0)