From 6f913cb85397ec846cc13761e08b1d145bd61fdb Mon Sep 17 00:00:00 2001 From: seonghun lee Date: Thu, 3 Sep 2026 22:03:34 +0900 Subject: [PATCH] Consider segment boundaries when checking for a missing optional ancestor isNestedBeneathMissingOptionalField used a plain string prefix test, so a required field whose path merely shared a string prefix with an absent optional field's path (for example optional 'a' and required 'ab') was treated as nested beneath it and its absence was never reported. Require the path to continue with a '.' or '[' segment boundary after the ancestor path. See gh-1057 Signed-off-by: seonghun lee --- .../restdocs/payload/JsonContentHandler.java | 6 +++++- .../payload/JsonContentHandlerTests.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 008f6366..0f2932f8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -75,7 +75,7 @@ private boolean isNestedBeneathMissingOptionalField(FieldDescriptor descriptor, List candidates = new ArrayList<>(this.fieldDescriptors); candidates.remove(descriptor); for (FieldDescriptor candidate : candidates) { - if (candidate.isOptional() && descriptor.getPath().startsWith(candidate.getPath()) + if (candidate.isOptional() && isNestedBeneath(candidate.getPath(), descriptor.getPath()) && isMissing(candidate, payload)) { return true; } @@ -83,6 +83,10 @@ && isMissing(candidate, payload)) { return false; } + private boolean isNestedBeneath(String ancestorPath, String path) { + return path.equals(ancestorPath) || path.startsWith(ancestorPath + ".") || path.startsWith(ancestorPath + "["); + } + private boolean isMissing(FieldDescriptor candidate, Object payload) { if (!this.fieldProcessor.hasField(candidate.getPath(), payload)) { return true; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java index de61f3be..e421ab31 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java @@ -158,6 +158,26 @@ void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsNotPresentIsN assertThat(missingFields.size()).isEqualTo(0); } + @Test + void describedFieldThatIsNotPresentWithPathPrefixedByPathOfMissingOptionalFieldIsConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), + new FieldDescriptor("ab")); + List missingFields = new JsonContentHandler("{\"c\":\"charlie\"}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(1); + assertThat(missingFields.get(0).getPath()).isEqualTo("ab"); + } + + @Test + void describedNestedFieldThatIsNotPresentWithPathPrefixedByPathOfMissingOptionalFieldIsConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a.b").optional(), + new FieldDescriptor("a.bc")); + List missingFields = new JsonContentHandler("{\"a\":{}}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(1); + assertThat(missingFields.get(0).getPath()).isEqualTo("a.bc"); + } + @Test void describedFieldThatIsNotPresentNestedBeneathOptionalArrayThatIsEmptyIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("outer"),