Let's say we have the following API:
@RestController
public class PersonController {
@GetMapping("/api/person")
public List<Person> findPeople() {
return Arrays.asList(new Person("Doe", "Foo", "John"), new Person("Doe", null, "Jane"));
}
}
Where the model class looks like this:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
private String lastName;
private String middleName;
private String firstName;
}
As you can see in the controller, the middleName
property of the second object in the list is null
. I'm now trying to use Spring REST Docs to document the properties of the API, so I wrote the following test:
@Test
public void findAllReturnsPeople() throws Exception {
mockMvc.perform(get("/api/person").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.[0].firstName", is("John")))
.andExpect(jsonPath("$.[0].middleName", is("Foo")))
.andExpect(jsonPath("$.[0].lastName", is("Doe")))
.andExpect(jsonPath("$.[1].firstName", is("Jane")))
.andExpect(jsonPath("$.[1].middleName", nullValue()))
.andExpect(jsonPath("$.[1].lastName", is("Doe")))
.andDo(document("person-get", responseFields(
fieldWithPath("[].firstName").description("The given name of a person"),
fieldWithPath("[].middleName").description("The optionally given middle name of a person"),
fieldWithPath("[].lastName").description("The last- or family name of a person"))));
}
By using the responseFields()
and fieldWithPath()
properties, I'm trying to give a description to each field. However, this approach fails for the middle name, throwing the following exception:
org.springframework.restdocs.snippet.SnippetException: Fields with the following paths were not found in the payload: [[].middleName]
at org.springframework.restdocs.payload.AbstractFieldsSnippet.validateFieldDocumentation(AbstractFieldsSnippet.java:257)
at org.springframework.restdocs.payload.AbstractFieldsSnippet.createModel(AbstractFieldsSnippet.java:167)
at org.springframework.restdocs.snippet.TemplatedSnippet.document(TemplatedSnippet.java:83)
at org.springframework.restdocs.generate.RestDocumentationGenerator.handle(RestDocumentationGenerator.java:206)
at org.springframework.restdocs.mockmvc.RestDocumentationResultHandler.handle(RestDocumentationResultHandler.java:55)
at org.springframework.test.web.servlet.MockMvc$1.andDo(MockMvc.java:183)
at be.g00glen00b.apps.demoempty.DemoEmptyApplicationTests.findAllReturnsPeople(DemoEmptyApplicationTests.java:37)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
However, when I change the code and change the middle name of the second Person
object to "Bar"
, the error disappears, and the snippet is correctly generated.
Additionally, when I remove the fieldWithPath("[].middleName")
code, and I run the test, I get the following exception:
org.springframework.restdocs.snippet.SnippetException: The following parts of the payload were not documented:
[ {
"middleName" : "Foo"
}, {
"middleName" : null
} ]
Which makes sense, since the middle name property isn't documented in that case.
My question is how can I prevent this error and document what the middleName
property does?
This is what I tried so far:
[0].middleName
(this leads to the same exception)JsonFieldType.STRING
or JsonFieldType.VARIES
(this also leads to the same exception)You need to tell REST Docs that the field is optional. You can do this by calling the optional()
method on the FieldDescriptor
:
.andDo(document("person-get", responseFields(
fieldWithPath("[].firstName").description("The given name of a person"),
fieldWithPath("[].middleName").description("The optionally given middle name of a person").optional(),
fieldWithPath("[].lastName").description("The last- or family name of a person"))));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With