Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

document hierarchical JSON payload with Spring REST Docs

I started to use Spring REST Docs to document a simple REST API. I have a payload that has some hierarchical structure, for example like this (company with employees).

{
    "companyName": "FooBar",
    "employee": 
    [
        {
            "name": "Lorem",
            "age": "42"
        },

        {
            "name": "Ipsum",
            "age": "24"
        }
    ]
}

I would like to separate the documentation of the company object (name and array of employees) and the employee object (employee name and age).

Using the org.springframework.restdocs.payload.PayloadDocumentation.responseFields like explained here forces me to document all fields but in case I only want to document the employee fields - how can I achieve this?

I have no problem to document the company without the employee details because if a field is document the descendants are treated as been documented also. But I can not document the employee structure on its own and I have no dedicated payload for this structure without the company root object.

like image 743
FrVaBe Avatar asked Feb 08 '23 22:02

FrVaBe


1 Answers

Inspired by this question, I've implemented an enhancement which makes the original answer (see below) obsolete.

If you use 1.0.0.BUILD-SNAPSHOT (available from https://repo.spring.io/libs-snapshot), you can now mark a field as ignored. Ignored fields count has having been documented without actually appearing in the documentation.

Given that you want to separate the documentation, having two document calls makes sense. In the first you can document the company name and the array of employees. In the second you document the employees array and mark the company name as ignored.

Your test would look something like this:

mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andDo(document("company",
                responseFields(
                        fieldWithPath("companyName").description(
                                "The name of the company"),
                        fieldWithPath("employee").description(
                                "An array of the company's employees"))))
        .andDo(document("employee",
                responseFields(
                        fieldWithPath("companyName").ignored(),
                        fieldWithPath("employee[].name").description(
                                "The name of the employee"),
                        fieldWithPath("employee[].age").description(
                                "The age of the employee"))));

You'll end up with two directories of snippets, one named company and one named employee. You can then use the response-fields.adoc snippet from each.

Original answer

There's no explicit support for ignoring a field when you're documenting a request or a response, but I think you can probably achieve what you want by using a preprocessor to remove the fields that you don't want to document.

Given that you want to separate the documentation, having two document calls makes sense. In the first you can document the company name and the array of employees. In the second you need to preprocess the request to remove the company and then document the employees array.

Your test would look something like this:

mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andDo(document("company",
                responseFields(
                        fieldWithPath("companyName").description(
                                "The name of the company"),
                        fieldWithPath("employee").description(
                                "An array of the company's employees"))))
        .andDo(document("employee",
                preprocessResponse(removeCompany()),
                responseFields(
                        fieldWithPath("employee[].name").description(
                                "The name of the employee"),
                        fieldWithPath("employee[].age").description(
                                "The age of the employee"))));

Note the use of preprocessResponse in the second document call. removeCompany returns a preprocessor that uses a custom ContentModifier to remove company name from the response:

private OperationPreprocessor removeCompany() {
    return new ContentModifyingOperationPreprocessor(new ContentModifier() {

        @Override
        public byte[] modifyContent(byte[] originalContent, MediaType contentType) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                Map<?, ?> map = objectMapper.readValue(originalContent, Map.class);
                map.remove("companyName");
                return objectMapper.writeValueAsBytes(map);
            }
            catch (IOException ex) {
                return originalContent;
            }
        }

    });
}

You'll end up with two directories of snippets, one named company and one named employee. You can then use the response-fields.adoc snippet from each.

While the above will work, it's harder than it needs to be. I've opened an issue so that the preprocessing to modify the response's content will no longer be necessary.

like image 149
Andy Wilkinson Avatar answered Feb 11 '23 14:02

Andy Wilkinson