Fix JSON field type resolution for top-level and nested array fields
Previously, the logic that determined the field’s type would incorrectly look at the contents of the array. For example, if the array contained one or more objects, the field’s type would be resolved as Object rather than Array. This commit updates JsonFieldPath to record when a path explicitly identifies an array so that the array itself is used to determine the field’s type rather than its contents. Closes gh-292
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -42,16 +43,24 @@ final class JsonFieldPath {
|
||||
|
||||
private final boolean precise;
|
||||
|
||||
private JsonFieldPath(String rawPath, List<String> segments, boolean precise) {
|
||||
private final boolean array;
|
||||
|
||||
private JsonFieldPath(String rawPath, List<String> segments, boolean precise,
|
||||
boolean array) {
|
||||
this.rawPath = rawPath;
|
||||
this.segments = segments;
|
||||
this.precise = precise;
|
||||
this.array = array;
|
||||
}
|
||||
|
||||
boolean isPrecise() {
|
||||
return this.precise;
|
||||
}
|
||||
|
||||
boolean isArray() {
|
||||
return this.array;
|
||||
}
|
||||
|
||||
List<String> getSegments() {
|
||||
return this.segments;
|
||||
}
|
||||
@@ -63,7 +72,8 @@ final class JsonFieldPath {
|
||||
|
||||
static JsonFieldPath compile(String path) {
|
||||
List<String> segments = extractSegments(path);
|
||||
return new JsonFieldPath(path, segments, matchesSingleValue(segments));
|
||||
return new JsonFieldPath(path, segments, matchesSingleValue(segments),
|
||||
isArraySegment(segments.get(segments.size() - 1)));
|
||||
}
|
||||
|
||||
static boolean isArraySegment(String segment) {
|
||||
@@ -71,8 +81,9 @@ final class JsonFieldPath {
|
||||
}
|
||||
|
||||
static boolean matchesSingleValue(List<String> segments) {
|
||||
for (String segment : segments) {
|
||||
if (isArraySegment(segment)) {
|
||||
Iterator<String> iterator = segments.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (isArraySegment(iterator.next()) && iterator.hasNext()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ final class JsonFieldProcessor {
|
||||
if (matches.isEmpty()) {
|
||||
throw new FieldDoesNotExistException(path);
|
||||
}
|
||||
if (path.isPrecise()) {
|
||||
if ((!path.isArray()) && path.isPrecise()) {
|
||||
return matches.get(0);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -44,7 +44,7 @@ class JsonFieldTypeResolver {
|
||||
}
|
||||
return commonType;
|
||||
}
|
||||
return determineFieldType(this.fieldProcessor.extract(fieldPath, payload));
|
||||
return determineFieldType(field);
|
||||
}
|
||||
|
||||
private JsonFieldType determineFieldType(Object fieldValue) {
|
||||
|
||||
@@ -32,43 +32,59 @@ import static org.junit.Assert.assertTrue;
|
||||
public class JsonFieldPathTests {
|
||||
|
||||
@Test
|
||||
public void singleFieldIsPrecise() {
|
||||
assertTrue(JsonFieldPath.compile("a").isPrecise());
|
||||
public void singleFieldIsPreciseAndNotAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("a");
|
||||
assertTrue(path.isPrecise());
|
||||
assertFalse(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleNestedFieldIsPrecise() {
|
||||
assertTrue(JsonFieldPath.compile("a.b").isPrecise());
|
||||
public void singleNestedFieldIsPreciseAndNotAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("a.b");
|
||||
assertTrue(path.isPrecise());
|
||||
assertFalse(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topLevelArrayIsNotPrecise() {
|
||||
assertFalse(JsonFieldPath.compile("[]").isPrecise());
|
||||
public void topLevelArrayIsPreciseAndAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("[]");
|
||||
assertTrue(path.isPrecise());
|
||||
assertTrue(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fieldBeneathTopLevelArrayIsNotPrecise() {
|
||||
assertFalse(JsonFieldPath.compile("[]a").isPrecise());
|
||||
public void fieldBeneathTopLevelArrayIsNotPreciseAndNotAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("[]a");
|
||||
assertFalse(path.isPrecise());
|
||||
assertFalse(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayIsNotPrecise() {
|
||||
assertFalse(JsonFieldPath.compile("a[]").isPrecise());
|
||||
public void arrayIsPreciseAndAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("a[]");
|
||||
assertTrue(path.isPrecise());
|
||||
assertTrue(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedArrayIsNotPrecise() {
|
||||
assertFalse(JsonFieldPath.compile("a.b[]").isPrecise());
|
||||
public void nestedArrayIsPreciseAndAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("a.b[]");
|
||||
assertTrue(path.isPrecise());
|
||||
assertTrue(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayOfArraysIsNotPrecise() {
|
||||
assertFalse(JsonFieldPath.compile("a[][]").isPrecise());
|
||||
public void arrayOfArraysIsNotPreciseAndIsAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("a[][]");
|
||||
assertFalse(path.isPrecise());
|
||||
assertTrue(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fieldBeneathAnArrayIsNotPrecise() {
|
||||
assertFalse(JsonFieldPath.compile("a[].b").isPrecise());
|
||||
public void fieldBeneathAnArrayIsNotPreciseAndIsNotAnArray() {
|
||||
JsonFieldPath path = JsonFieldPath.compile("a[].b");
|
||||
assertFalse(path.isPrecise());
|
||||
assertFalse(path.isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -55,6 +56,17 @@ public class JsonFieldProcessorTests {
|
||||
equalTo((Object) "bravo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractTopLevelArray() {
|
||||
List<Map<String, Object>> payload = new ArrayList<>();
|
||||
Map<String, Object> bravo = new HashMap<>();
|
||||
bravo.put("b", "bravo");
|
||||
payload.add(bravo);
|
||||
payload.add(bravo);
|
||||
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("[]"), payload),
|
||||
equalTo((Object) payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractArray() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -45,6 +46,22 @@ public class JsonFieldTypeResolverTests {
|
||||
assertFieldType(JsonFieldType.ARRAY, "[]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topLevelArray() throws IOException {
|
||||
assertThat(
|
||||
this.fieldTypeResolver.resolveFieldType("[]",
|
||||
new ObjectMapper().readValue("[{\"a\":\"alpha\"}]", List.class)),
|
||||
equalTo(JsonFieldType.ARRAY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedArray() throws IOException {
|
||||
assertThat(
|
||||
this.fieldTypeResolver.resolveFieldType("a[]",
|
||||
createPayload("{\"a\": [{\"b\":\"bravo\"}]}")),
|
||||
equalTo(JsonFieldType.ARRAY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void booleanField() throws IOException {
|
||||
assertFieldType(JsonFieldType.BOOLEAN, "true");
|
||||
|
||||
@@ -67,13 +67,14 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
|
||||
public void arrayRequestWithFields() throws IOException {
|
||||
this.snippet.expectRequestFields()
|
||||
.withContents(tableWithHeader("Path", "Type", "Description")
|
||||
.row("`[]a.b`", "`Number`", "one")
|
||||
.row("`[]a.c`", "`String`", "two")
|
||||
.row("`[]a`", "`Object`", "three"));
|
||||
.row("`[]`", "`Array`", "one").row("`[]a.b`", "`Number`", "two")
|
||||
.row("`[]a.c`", "`String`", "three")
|
||||
.row("`[]a`", "`Object`", "four"));
|
||||
|
||||
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"),
|
||||
fieldWithPath("[]a.c").description("two"),
|
||||
fieldWithPath("[]a").description("three")))
|
||||
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"),
|
||||
fieldWithPath("[]a.b").description("two"),
|
||||
fieldWithPath("[]a.c").description("three"),
|
||||
fieldWithPath("[]a").description("four")))
|
||||
.document(this.operationBuilder.request("http://localhost")
|
||||
.content(
|
||||
"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")
|
||||
@@ -238,7 +239,7 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
|
||||
.requestFields(fieldWithPath("a.b").description("one"),
|
||||
fieldWithPath("a.c").description("two"))
|
||||
.and(fieldWithPath("a").description("three"))
|
||||
.document(operationBuilder.request("http://localhost")
|
||||
.document(this.operationBuilder.request("http://localhost")
|
||||
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
|
||||
}
|
||||
|
||||
@@ -252,7 +253,7 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
|
||||
PayloadDocumentation.requestFields(fieldWithPath("a").description("one"))
|
||||
.andWithPrefix("a.", fieldWithPath("b").description("two"),
|
||||
fieldWithPath("c").description("three"))
|
||||
.document(operationBuilder.request("http://localhost")
|
||||
.document(this.operationBuilder.request("http://localhost")
|
||||
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
|
||||
|
||||
new RequestFieldsSnippet(Arrays.asList(
|
||||
fieldWithPath("Foo|Bar").type("one|two").description("three|four")))
|
||||
.document(operationBuilder.request("http://localhost")
|
||||
.document(this.operationBuilder.request("http://localhost")
|
||||
.content("{\"Foo|Bar\": 5}").build());
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests {
|
||||
.withContents(tableWithHeader("Path", "Type", "Description")
|
||||
.row("`id`", "`Number`", "one").row("`date`", "`String`", "two")
|
||||
.row("`assets`", "`Array`", "three")
|
||||
.row("`assets[]`", "`Object`", "four")
|
||||
.row("`assets[]`", "`Array`", "four")
|
||||
.row("`assets[].id`", "`Number`", "five")
|
||||
.row("`assets[].name`", "`String`", "six"));
|
||||
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"),
|
||||
@@ -90,7 +90,7 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests {
|
||||
public void arrayResponse() throws IOException {
|
||||
this.snippet.expectResponseFields()
|
||||
.withContents(tableWithHeader("Path", "Type", "Description").row("`[]`",
|
||||
"`String`", "one"));
|
||||
"`Array`", "one"));
|
||||
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one")))
|
||||
.document(this.operationBuilder.response()
|
||||
.content("[\"a\", \"b\", \"c\"]").build());
|
||||
@@ -291,7 +291,7 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests {
|
||||
.withContents(tableWithHeader("Path", "Type", "Description")
|
||||
.row("`id`", "`Number`", "one").row("`date`", "`String`", "two")
|
||||
.row("`assets`", "`Array`", "three")
|
||||
.row("`assets[]`", "`Object`", "four")
|
||||
.row("`assets[]`", "`Array`", "four")
|
||||
.row("`assets[].id`", "`Number`", "five")
|
||||
.row("`assets[].name`", "`String`", "six"));
|
||||
PayloadDocumentation
|
||||
|
||||
Reference in New Issue
Block a user