|
34 | 34 | * {@link KeywordValidator} for anyOf. |
35 | 35 | */ |
36 | 36 | public class AnyOfValidator extends BaseKeywordValidator { |
37 | | - private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation"; |
38 | | - |
39 | 37 | private final List<Schema> schemas; |
40 | 38 |
|
41 | 39 | private Boolean canShortCircuit = null; |
@@ -71,7 +69,8 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo |
71 | 69 | } |
72 | 70 | int numberOfValidSubSchemas = 0; |
73 | 71 | List<Error> existingErrors = executionContext.getErrors(); |
74 | | - List<Error> allErrors = null; |
| 72 | + List<Error> allErrors = null; // Keeps track of all the errors for reporting if in the end none of the schemas match |
| 73 | + List<Error> discriminatorErrors = null; |
75 | 74 | List<Error> errors = new ArrayList<>(); |
76 | 75 | executionContext.setErrors(errors); |
77 | 76 | try { |
@@ -109,64 +108,97 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo |
109 | 108 |
|
110 | 109 | if (errors.isEmpty() && (!this.schemaContext.isDiscriminatorKeywordEnabled()) |
111 | 110 | && canShortCircuit() && canShortCircuit(executionContext)) { |
112 | | - // Clear all errors. Note that this is checked in finally. |
113 | | - allErrors = null; |
| 111 | + // Successful so return only the existing errors, ie. no new errors |
114 | 112 | executionContext.setErrors(existingErrors); |
115 | 113 | return; |
116 | 114 | } else if (this.schemaContext.isDiscriminatorKeywordEnabled()) { |
117 | | - DiscriminatorContext currentDiscriminatorContext = executionContext.getCurrentDiscriminatorContext(); |
118 | | - if (currentDiscriminatorContext.isDiscriminatorMatchFound() |
119 | | - || currentDiscriminatorContext.isDiscriminatorIgnore()) { |
120 | | - if (!errors.isEmpty()) { |
121 | | - // The following is to match the previous logic adding to all errors |
122 | | - // which is generally discarded as it returns errors but the allErrors |
123 | | - // is getting processed in finally |
124 | | - if (allErrors == null) { |
125 | | - allErrors = new ArrayList<>(); |
126 | | - } |
127 | | - allErrors.add(error().instanceNode(node).instanceLocation(instanceLocation) |
128 | | - .locale(executionContext.getExecutionConfig().getLocale()) |
129 | | - .arguments(DISCRIMINATOR_REMARK) |
130 | | - .build()); |
131 | | - } else { |
132 | | - // Clear all errors. Note that this is checked in finally. |
133 | | - allErrors = null; |
134 | | - } |
135 | | - existingErrors.addAll(errors); |
136 | | - executionContext.setErrors(existingErrors); |
137 | | - return; |
| 115 | + JsonNode refNode = schema.getSchemaNode().get("$ref"); |
| 116 | + DiscriminatorState state = executionContext.getDiscriminatorMapping().get(instanceLocation); |
| 117 | + boolean discriminatorMatchFound = false; |
| 118 | + if (refNode != null) { |
| 119 | + // Check if there is a match |
| 120 | + String discriminatingValue = state.getDiscriminatingValue(); |
| 121 | + if (discriminatingValue != null) { |
| 122 | + String ref = refNode.asText(); |
| 123 | + if (state.isExplicitMapping() && ref.equals(discriminatingValue)) { |
| 124 | + // Explicit matching |
| 125 | + discriminatorMatchFound = true; |
| 126 | + state.setMatch(ref); |
| 127 | + } else if (!state.isExplicitMapping() && ref.endsWith(discriminatingValue)) { |
| 128 | + // Implicit matching |
| 129 | + discriminatorMatchFound = true; |
| 130 | + state.setMatch(ref); |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + if (discriminatorMatchFound) { |
| 135 | + /* |
| 136 | + * Note that discriminator cannot change the outcome of the evaluation but can be used to filter off |
| 137 | + * any additional messages |
| 138 | + */ |
| 139 | + if (!errors.isEmpty()) { |
| 140 | + /* |
| 141 | + * This means that the discriminated value has errors and doesn't match so these errors |
| 142 | + * are the only ones that will be reported *IF* there are no other schemas that successfully |
| 143 | + * validate to meet the requirement of anyOf. |
| 144 | + * |
| 145 | + * If there are any successful schemas as per anyOf, all these errors will be discarded. |
| 146 | + */ |
| 147 | + discriminatorErrors = new ArrayList<>(errors); |
| 148 | + allErrors = null; // This is no longer needed |
| 149 | + } |
138 | 150 | } |
139 | 151 | } |
140 | | - if (allErrors == null) { |
141 | | - allErrors = new ArrayList<>(); |
| 152 | + |
| 153 | + /* |
| 154 | + * This adds all the errors for this schema to the list that contains all the errors for later reporting. |
| 155 | + * |
| 156 | + * There's no need to add these if there was a discriminator match with errors as only the discriminator |
| 157 | + * errors will be reported if all the schemas fail. |
| 158 | + */ |
| 159 | + if (!errors.isEmpty() && discriminatorErrors == null) { |
| 160 | + if (allErrors == null) { |
| 161 | + allErrors = new ArrayList<>(); |
| 162 | + } |
| 163 | + allErrors.addAll(errors); |
142 | 164 | } |
143 | | - allErrors.addAll(errors); |
144 | 165 | } |
145 | 166 | } finally { |
146 | 167 | // Restore flag |
147 | 168 | executionContext.setFailFast(failFast); |
148 | 169 | } |
149 | 170 |
|
150 | | - if (this.schemaContext.isDiscriminatorKeywordEnabled() |
151 | | - && executionContext.getCurrentDiscriminatorContext().isActive() |
152 | | - && !executionContext.getCurrentDiscriminatorContext().isDiscriminatorIgnore()) { |
153 | | - existingErrors.add(error().instanceNode(node).instanceLocation(instanceLocation) |
154 | | - .locale(executionContext.getExecutionConfig().getLocale()) |
155 | | - .arguments( |
156 | | - "based on the provided discriminator. No alternative could be chosen based on the discriminator property") |
157 | | - .build()); |
158 | | - executionContext.setErrors(existingErrors); |
159 | | - return; |
| 171 | + if (this.schemaContext.isDiscriminatorKeywordEnabled()) { |
| 172 | + // https://spec.openapis.org/oas/v3.1.0#discriminator-object |
| 173 | + // If the discriminator value does not match an implicit or explicit mapping, no schema can be determined and validation SHOULD fail. Mapping keys MUST be string values, but tooling MAY convert response values to strings for comparison. |
| 174 | + |
| 175 | + /* |
| 176 | + * The only case where the discriminator can change the outcome of the result is if the discriminator value does not match an implicit or explicit mapping |
| 177 | + */ |
| 178 | + DiscriminatorState state = executionContext.getDiscriminatorMapping().get(instanceLocation); |
| 179 | + if (state != null && state.getMatch() == null && state.getPropertyValue() != null) { |
| 180 | + // state.getPropertyValue() == null means there is no value at propertyName |
| 181 | + existingErrors.add(error().keyword("discriminator").instanceNode(node).instanceLocation(instanceLocation) |
| 182 | + .locale(executionContext.getExecutionConfig().getLocale()) |
| 183 | + .arguments( |
| 184 | + "based on the provided discriminator. No alternative could be chosen based on the discriminator property") |
| 185 | + .build()); |
| 186 | + } |
160 | 187 | } |
161 | 188 | } finally { |
162 | 189 | if (this.schemaContext.isDiscriminatorKeywordEnabled()) { |
163 | 190 | executionContext.leaveDiscriminatorContextImmediately(instanceLocation); |
164 | 191 | } |
165 | 192 | } |
166 | 193 | if (numberOfValidSubSchemas >= 1) { |
| 194 | + // Successful so return only the existing errors, ie. no new errors |
167 | 195 | executionContext.setErrors(existingErrors); |
168 | 196 | } else { |
169 | | - if (allErrors != null) { |
| 197 | + if (discriminatorErrors != null) { |
| 198 | + // If errors are present matching the discriminator, only these errors should be reported |
| 199 | + existingErrors.addAll(discriminatorErrors); |
| 200 | + } else if (allErrors != null) { |
| 201 | + // As the anyOf has failed, report all the errors |
170 | 202 | existingErrors.addAll(allErrors); |
171 | 203 | } |
172 | 204 | executionContext.setErrors(existingErrors); |
|
0 commit comments