Skip to content

Commit 713f98f

Browse files
committed
feat: Retain oneOf/anyOf branch errors in validator
1 parent fb43e09 commit 713f98f

2 files changed

Lines changed: 56 additions & 8 deletions

File tree

src/main/java/com/retailsvc/http/validate/DefaultValidator.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,29 +527,39 @@ private Optional<ValidationError> checkAllOf(Object value, List<Schema> parts, S
527527
}
528528

529529
private Optional<ValidationError> checkAnyOf(Object value, List<Schema> options, String pointer) {
530+
List<ValidationError> failures = new ArrayList<>();
530531
for (Schema o : options) {
531-
if (check(value, o, pointer).isEmpty()) {
532+
Optional<ValidationError> result = check(value, o, pointer);
533+
if (result.isEmpty()) {
532534
return OK;
533535
}
536+
failures.add(result.get());
534537
}
535-
return err(pointer, "anyOf", "did not match any anyOf branch", value);
538+
return Optional.of(
539+
new ValidationError(pointer, "anyOf", "did not match any anyOf branch", value, failures));
536540
}
537541

538542
private Optional<ValidationError> checkOneOf(Object value, List<Schema> options, String pointer) {
539543
int matched = 0;
544+
List<ValidationError> failures = new ArrayList<>();
540545
for (Schema o : options) {
541-
if (check(value, o, pointer).isEmpty()) {
546+
Optional<ValidationError> result = check(value, o, pointer);
547+
if (result.isEmpty()) {
542548
matched++;
549+
} else {
550+
failures.add(result.get());
543551
}
544552
}
545553
if (matched == 1) {
546554
return OK;
547555
}
548-
return err(
549-
pointer,
550-
"oneOf",
551-
"matched " + matched + " of " + options.size() + " oneOf branches",
552-
value);
556+
return Optional.of(
557+
new ValidationError(
558+
pointer,
559+
"oneOf",
560+
"matched " + matched + " of " + options.size() + " oneOf branches",
561+
value,
562+
matched == 0 ? failures : List.of()));
553563
}
554564

555565
private Optional<ValidationError> checkNot(Object value, Schema inner, String pointer) {

src/test/java/com/retailsvc/http/validate/DefaultValidatorDispatchTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,42 @@ void neverSchemaRejectsNull() {
235235
.extracting(t -> ((ValidationException) t).error().keyword())
236236
.isEqualTo("false");
237237
}
238+
239+
@Test
240+
void oneOfZeroMatchesCapturesEachBranchError() {
241+
// "hello" (len 5): branch[0] minLength 100 fails, branch[1] maxLength 2 fails.
242+
var schema = new OneOfSchema(List.of(stringSchema(100, null), stringSchema(null, 2)), Map.of());
243+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
244+
.isInstanceOf(ValidationException.class)
245+
.satisfies(
246+
t -> {
247+
var err = ((ValidationException) t).error();
248+
assertThat(err.branches())
249+
.extracting(ValidationError::keyword)
250+
.containsExactly("minLength", "maxLength");
251+
});
252+
}
253+
254+
@Test
255+
void oneOfTwoMatchesHasNoBranchErrors() {
256+
// Both branches accept "hello" — ambiguity, not a field error.
257+
var schema = new OneOfSchema(List.of(stringSchema(null, 10), stringSchema(1, null)), Map.of());
258+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
259+
.isInstanceOf(ValidationException.class)
260+
.satisfies(t -> assertThat(((ValidationException) t).error().branches()).isEmpty());
261+
}
262+
263+
@Test
264+
void anyOfNoMatchCapturesEachBranchError() {
265+
var schema = new AnyOfSchema(List.of(stringSchema(100, null), stringSchema(null, 2)), Map.of());
266+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
267+
.isInstanceOf(ValidationException.class)
268+
.satisfies(
269+
t -> {
270+
var err = ((ValidationException) t).error();
271+
assertThat(err.branches())
272+
.extracting(ValidationError::keyword)
273+
.containsExactly("minLength", "maxLength");
274+
});
275+
}
238276
}

0 commit comments

Comments
 (0)