Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Documenting null values with Spring REST Docs

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:

  • Documenting only the non-null value, eg [0].middleName (this leads to the same exception)
  • Changing the type explicitely to JsonFieldType.STRINGor JsonFieldType.VARIES (this also leads to the same exception)
like image 490
g00glen00b Avatar asked Mar 06 '23 22:03

g00glen00b


1 Answers

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"))));
like image 113
Andy Wilkinson Avatar answered Mar 08 '23 23:03

Andy Wilkinson