Documenting null values with Spring REST Docs
Asked Answered
P

1

5

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)
Pelagic answered 28/6, 2018 at 9:1 Comment(0)
H
8

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"))));
Hazlett answered 28/6, 2018 at 9:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.