Sylvain Lemoine
Another dev blog in the nexus…

Spring REST docs tutorial

· by Sylvain Lemoine · Read in about 13 min · (2577 Words)
Java Spring Rest Docs Spring Framework Spring Boot

Intro

I was recently looking around to improve the api documentation we produce at work. We usually provides a swagger client to allow our Q&A staff to play with our api, but I always thought it lacks of some details / examples about how to use our api and it does not generates a final documentation which makes me enough satisfied for sharing it with external staff.

As our stack is almost exclusively spring-framework based, it was quite naturally that I gave a look to Spring Rest Docs.

Two points got my attention : - The purpose of documenting API through test - The discipline of writing strict documentation (by default), otherwise tests will not pass.

For this article I use Spring Rest Docs with the following basics:

  • Spring MVC
  • MockMvc
  • Junit

Sample sources are available on my github at: https://github.com/slem1/spring-rest-docs-sample

Init the project

Let’s start a new spring boot web project.

   <dependencyManagement>
       <dependencies>
           <!-- Spring -->
           <dependency>
               <!-- Import dependency management from Spring Boot -->
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-dependencies</artifactId>
               <version>1.5.4.RELEASE</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
       </dependencies>
   </dependencyManagement>

   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <version>1.4.4.RELEASE</version>
               <executions>
                   <execution>
                       <goals>
                           <goal>build-info</goal>
                           <goal>repackage</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
       </plugins>
   </build>

We use MockMvc for our tests and documentation, so add the dependency to spring-restdocs-mockmvc

        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-mockmvc</artifactId>
            <version>1.2.1.RELEASE</version>
            <scope>test</scope>            
        </dependency>

        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-core</artifactId>
            <version>1.2.1.RELEASE</version>
            <scope>test</scope>
        </dependency>

And the ascii doctor plugin which will generate the final documentation. We hook the plugin to the prepare-package phase, so mvn package command will produce the api documentation

        <plugin>
            <groupId>org.asciidoctor</groupId>
            <artifactId>asciidoctor-maven-plugin</artifactId>
            <version>1.5.3</version>
            <executions>
                <execution>
                    <id>generate-docs</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>process-asciidoc</goal>
                    </goals>
                    <configuration>
                        <backend>html</backend>
                        <doctype>book</doctype>
                    </configuration>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.restdocs</groupId>
                    <artifactId>spring-restdocs-asciidoctor</artifactId>
                    <version>1.2.1.RELEASE</version>
                </dependency>
            </dependencies>
        </plugin>      

Create the api

We’ll make an API for managing our Mobile Suit factory. Mobile suit is the name for mechas (large armoured robot) in the popular anime Gundam series.

First, The Mobile Suit’s class

public class MobileSuit implements Serializable {

    private Long id;

    private String modelName;

    private List<String> weapons;

    //... Getters and setters here
}

And the corresponding DTO class for creating new mobile suit

public class MobileSuitPostDto implements Serializable {

    private String modelName;

    private List<String> weapons;

}

And here our (super) simple API for managing our mobile suits

@RequestMapping(MobileSuitFactoryController.API_ROOT_RESOURCE)
@RestController
public class MobileSuitFactoryController {

    public static final String API_ROOT_RESOURCE = "/mobilesuits";

    public static final String SEARCH_RESOURCE = "/search";

    public static final String PARAM_ID = "idmobilesuit";

    public static final String PARAM_MODEL_NAME = "modelname";

    private List<MobileSuit> mobileSuits;

    public MobileSuitFactoryController(){
        final MobileSuit mobileSuit1 = new MobileSuit();
        mobileSuit1.setId(1L);
        mobileSuit1.setModelName("RX-78");
        mobileSuit1.setWeapons(Arrays.asList("Saber", "Rifle"));

        final MobileSuit mobileSuit2 = new MobileSuit();
        mobileSuit2.setId(2L);
        mobileSuit2.setModelName("MS-06 Zaku II");
        mobileSuit2.setWeapons(Arrays.asList("Cannon"));

        mobileSuits = new ArrayList<>();
        mobileSuits.add(mobileSuit1);
        mobileSuits.add(mobileSuit2);
    }

    /**
     * Get all existing mobile suits
     * @return List of MobileSuit
     */
    @GetMapping
    public List<MobileSuit> getAll(){
        return mobileSuits;
    }


    /**
     * Search Mobile suit for which model name starts with...
     *
     * @param modelName model name
     * @return List of MobileSuit
     */
    @GetMapping(SEARCH_RESOURCE)
    public List<MobileSuit> searchByModelName(@RequestParam(PARAM_MODEL_NAME) String modelName){
        return mobileSuits.parallelStream()
                .filter(m -> m.getModelName().toUpperCase().startsWith(modelName.toUpperCase()))
                .collect(Collectors.toList());
    }


    /**
     * Get Mobile Suit by id
     *
     * @param idMobileSuit mobile suit identifier
     * @return MobileSuit
     */
    @GetMapping("/{" + PARAM_ID + "}")
    public MobileSuit getById(@PathVariable(PARAM_ID) Long idMobileSuit){
        return mobileSuits.parallelStream()
                .filter(m -> m.getId().equals(idMobileSuit))
                .findFirst()
                .orElse(null);
    }


       /**
        * create Mobile Suit
        *
        * @param mobileSuitPostDto mobile suit info
        */
       @PostMapping
       public void create(@RequestBody MobileSuitPostDto mobileSuitPostDto) {

           //yeaaah, it's not thread safe
           Long maxId = this.mobileSuits.stream()
                   .max(Comparator.comparingLong(MobileSuit::getId))
                   .map(MobileSuit::getId)
                   .orElse(0L);

           MobileSuit mobileSuit = new MobileSuit();
           mobileSuit.setId(maxId + 1);
           mobileSuit.setModelName(mobileSuitPostDto.getModelName());
           mobileSuit.setWeapons(mobileSuitPostDto.getWeapons());
           this.mobileSuits.add(mobileSuit);
       }

}

and the Application class :

@SpringBootApplication
public class Application {

    public static void main(String... args){

        SpringApplication.run(Application.class);
    }
}

Write the documentation

first generation

API is ready, now let’s dig into Spring Rest Docs.

Spring Rest Docs is documentation through test project. The good point about this is that as you never in your whole life of developer deliver an api without testing it (am I right ? ;)) you have no more excuse to not document it nicely.

We create the following testing class for our controller.

@RunWith(SpringRunner.class)
public class MobileSuitFactoryControllerRestDocsTest {  

    private MockMvc mockMvc;

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();


    @Before
     public void before() {
         mockMvc = MockMvcBuilders.standaloneSetup(new MobileSuitFactoryController())
                 .apply(MockMvcRestDocumentation.documentationConfiguration(this.restDocumentation))                
                 .build();
     }

    @Test
    public void getAll() {
        //TODO Write the test
    }
}

Note that in the above code we add a JUnit rule to manage the documentation context for each test. The «target/generated-snippets» path will be the default directory where documentation snippets will be generated. You can change it through JUnitRestDocumentation constructor.

As we will see through this article Spring Rest Docs is a matter of snippets generation (response-fields, request-fields…) and references to these snippets in the final documentation api. The default format for generated snippets and documentation will be asciidoctor.

We also build the MockMvc object in the before method by applying MockMvcRestDocumentation.documentationConfiguration with a reference to the JUnitRestDocumentation rule.

Now, let’s fill in our first test:

 @Test
 public void getAll() throws Exception {
        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("getAll"));
 }

It looks like a classic MockMvc test and it is except that we use the RequestBuilders provided by restdocs-mockmvc to build the client request. Besides we add and extra (andDo) operation add the hand, the one which will generate the snippets.

Run the test

It should generates a bunch of snippets in target/generated-snippets path if you keep the default. In our case the snippets will be stored under the getAll directory:

 generated-snippets/
 └── getAll
     ├── curl-request.adoc
     ├── httpie-request.adoc
     ├── http-request.adoc
     ├── http-response.adoc
     ├── request-body.adoc
     └── response-body.adoc

Now we have a bunch of generated snippets, in asciidoctor format. Nice !

Let’s generate the final documentation: Add a file name api-doc.adoc in src/main/asciidoc/

src/
├── main
│   ├── asciidoc
│   │   └── api-doc.adoc
│   └── java
│       └── fr
│           └── sle
.                ├── Application.java
.                ├── ...
.

and package the app with maven

mvn package

You should now get a api-doc.html in target/generated-docs/ directory.

open it with you browser and… TADAM ! nothing… It’s normal since we did not refer any of our generated snippets !

To do so, let’s add the following include macro in the api-doc.adoc file.

== Retrieve all mobile suits

Example of curl command:

include::{snippets}/getAll/curl-request.adoc[]

Example of http request:

include::{snippets}/getAll/http-request.adoc[]

Example of http response:

include::{snippets}/getAll/http-response.adoc[]

Response body:

include::{snippets}/getAll/response-body.adoc[]

And mvn package the app once again. Of course, you can customize the documentation by choosing only the snippets which fit your needs.

Example of result:

Snippets folders structure

Before we see how to add request and response documentation and before the project grows bigger, let’s focus on the organization of our snippets.

As you may have guessed,

           .andDo(MockMvcRestDocumentation.document("getAll"));

has produced the getAll directory in the generated-snippets/ folder.

Naming all snippets like this can be cumbersome and error prones. Besides keeping all generated snippets on the same level across testing classes can be confusing.

You can prevent this by using parameters for output directory.

For instance I am used to use the following pattern : - {ClassName}/{methodName} : The unmodified simple name of the test class/The unmodified name of the test method

So our test for the getAll method will now be generated in the following path: MobileSuitFactoryControllerRestDocsTest/getAll


      @Test
      public void getAll() throws Exception {
            mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE)
                    .accept(MediaType.APPLICATION_JSON))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}"));
        }

Do not forget to update the api-doc.adoc file to include updated snippets references accordingly.

Response and request payload documentation

Documenting request and response payload is an essential part of API documentation… and a big effort for documentation author.

Let’s review the signature of the document method for MockMvcRestDocumentation class.

public static RestDocumentationResultHandler document(String identifier,
			Snippet... snippets)

The second parameter allows to specify snippets. We will specify one snippet for Response Payload thus using the PayloadDocumentation factory class.

PayloadDocumentation class allows you to document response fields and request fields in multiple ways by building snippets description for both types of payload.

Response payload

We are going to document the getById response payload of our Mobile suit’s api.

let’s look at the responseFields method signature.

public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) {
		return responseFields(Arrays.asList(descriptors));
	}

FieldDescriptor… are the description of the response fields, so in the case of our api method the fields of a mobile suit object.

first we describe our mobile suit model in term of fields descriptors :

public class ModelDescription {

    private ModelDescription(){}

    /**
     * @return The full mobile suit description
     */
    public static FieldDescriptor[] mobileSuit() {
         return new FieldDescriptor[]{
                       PayloadDocumentation.fieldWithPath("id").description("The mobile suit's id"),
                       PayloadDocumentation.fieldWithPath("weapons").description("Array of mobile suit's weapons")
         };
    }
}

And write the first pieces of getById test

    @Test
    public void getById() throws Exception {
        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE+"/1")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

then add the payload documentation of response fields

   mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE+"/1")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                                        PayloadDocumentation.responseFields(ModelDescription.mobileSuit())));

and run the test…

…it failed !

org.springframework.restdocs.snippet.SnippetException: The following parts of the payload were not documented:
{
  "modelName" : "RX-78"
}

As you can see if you use responseFields method and omit some parts of your documentation the test will fail. In our case we miss the modelName field.

Update the fields description

 public static FieldDescriptor[] mobileSuit() {
        return new FieldDescriptor[]{
                PayloadDocumentation.fieldWithPath("id").description("The mobile suit's id"),
                PayloadDocumentation.fieldWithPath("modelName").description("The mobile suit's model name"),
                PayloadDocumentation.fieldWithPath("weapons").description("Array of mobile suit's weapons")
        };
    }

If you run the test now, it should succeed and generate a response-fields.adoc into generated-snippets/MobileSuitFactoryControllerRestDocsTest/getById/

Reference it in the api docs file with the include macro:

== Retrieve one mobile suit by its identifier

Response fields:
include::{snippets}/MobileSuitFactoryControllerRestDocsTest/getById/response-fields.adoc[]

and package the app once again. This is our documentation:

Collection of…

let’s go back to the getAll method documentation which returns a list of objects

One (verbose) simple way to handle collection of payload is to write a specific description

    public static FieldDescriptor[] listOfMobileSuits(){
        return new FieldDescriptor[]{
                PayloadDocumentation.fieldWithPath("[]").description("Array of mobile suits"),
                PayloadDocumentation.fieldWithPath("[].id").description("The mobile suit's id"),
                PayloadDocumentation.fieldWithPath("[].modelName").description("The mobile suit's model name"),
                PayloadDocumentation.fieldWithPath("[].weapons").description("Array of mobile suit's weapons")
        };
    }
    @Test
    public void getAll() throws Exception {
        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                     PayloadDocumentation.responseFields(ModelDescription.listOfMobileSuits())));
    }

but nobody likes to write things twice… so you can achieve the same result by reusing the mobile suit unitary description:

    @Test
    public void getAll() throws Exception {
        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                        PayloadDocumentation.responseFields(PayloadDocumentation.fieldWithPath("[]")
                                .description("Array of mobile suits")).andWithPrefix("[]", ModelDescription.mobileSuit())));
    }

Request payload

For request payload example, we document the create method of our Mobile suit’s api.

The way to achieve request payload documentation is pretty much the same as for response payload. The intersting thing here is the constraints documentation.

Add description in ModelDescription class for MobileSuitPostDto and a new test.

    public static FieldDescriptor[] mobileSuitPostDto() {
        return new FieldDescriptor[]{
                PayloadDocumentation.fieldWithPath("modelName").description("The mobile suit's model name"),
                PayloadDocumentation.fieldWithPath("weapons").description("Array of mobile suit's weapons")
        };
    }
    @Test
    public void create() throws Exception {
        MobileSuitPostDto mobileSuitPostDto = new MobileSuitPostDto();
        mobileSuitPostDto.setModelName("MS-09RS Rick Dom");
        mobileSuitPostDto.setWeapons(Arrays.asList("Heat Saber", "Scattering Beam Gun", "Machine Gun", "Bazooka"));
        ObjectMapper objectMapper = new ObjectMapper();
        String content = objectMapper.writeValueAsString(mobileSuitPostDto);

        mockMvc.perform(RestDocumentationRequestBuilders.post(MobileSuitFactoryController.API_ROOT_RESOURCE)
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .content(content))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}", PayloadDocumentation.requestFields(ModelDescription.mobileSuitPostDto())));

    }
Add constraints

Requests payloads usually have some constraints like not null fields, minimal array length…

Let’s add some bean validation on MobileSuitPostDto

public class MobileSuitPostDto implements Serializable {

    @NotNull
    private String modelName;

    @NotNull
    @Size(min = 1)
    private List<String> weapons;
}

Spring Rest docs provides ConstraintDescriptions class to access class’s constraints

ConstraintDescriptions userConstraints = new ConstraintDescriptions(MobileSuitPostDto.class);
List<String> descriptions = userConstraints.descriptionsForProperty("modelName");

By Using it you can add the constraints descriptions to the fields descriptions of your class.

  private static <T> String getConstraints(Class<T> clazz, String property){
        ConstraintDescriptions userConstraints = new ConstraintDescriptions(clazz);
        List<String> descriptions = userConstraints.descriptionsForProperty(property);

        StringJoiner stringJoiner = new StringJoiner(". ", "", ".");
        descriptions.forEach(stringJoiner::add);
        return stringJoiner.toString();
    }

    public static FieldDescriptor[] mobileSuitPostDto() {

        return new FieldDescriptor[]{
                PayloadDocumentation.fieldWithPath("modelName").description("The mobile suit's model name. " + getConstraints(MobileSuitPostDto.class, "modelName")),
                PayloadDocumentation.fieldWithPath("weapons").description("Array of mobile suit's weapons. " + getConstraints(MobileSuitPostDto.class, "weapons"))
        };
    }

The above getConstraints method is an homemade utility method to concatenate all constraints descriptions of a class.

Add the snippets the new snippets reference

== Create a new mobile suit

Request fields:

include::{snippets}/MobileSuitFactoryControllerRestDocsTest/create/request-fields.adoc[]

Example of documentation result:

Parameters

Path Parameters

let’s review our getById test method

    @Test
    public void getById() throws Exception {
        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE + "/1")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                        PayloadDocumentation.responseFields(ModelDescription.getMobileSuit())));
    }

And add the pathParameters documentation with RequestDocumentation.pathParameters

    @Test
    public void getById() throws Exception {

        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE
                + "/{" + MobileSuitFactoryController.PARAM_ID + "}", 1)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                        PayloadDocumentation.responseFields(ModelDescription.mobileSuit()),
                        RequestDocumentation.pathParameters(
                                RequestDocumentation.parameterWithName(MobileSuitFactoryController.PARAM_ID).description("Mobile suit's id"))));
    }

Running the test will produce path-parameters.adoc snippet

Request parameters

The same can be done for request parameters through RequestDocumentation.requestParameters.

We write the following test for the searchByModelName method :

  @Test
  public void searchByModelName() throws Exception {

        final MobileSuit expectedResult = new MobileSuit();
        expectedResult.setId(1L);
        expectedResult.setModelName("RX-78");
        expectedResult.setWeapons(Arrays.asList("Saber", "Rifle"));

        ObjectMapper objectMapper = new ObjectMapper();

        String expectedJson = objectMapper.writeValueAsString(Arrays.asList(expectedResult));

        mockMvc.perform(RestDocumentationRequestBuilders.get(MobileSuitFactoryController.API_ROOT_RESOURCE + MobileSuitFactoryController.SEARCH_RESOURCE)
                .accept(MediaType.APPLICATION_JSON)
                .param(MobileSuitFactoryController.PARAM_MODEL_NAME, "RX"))
                .andExpect(MockMvcResultMatchers.content().json(expectedJson))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                        RequestDocumentation.requestParameters(
                                RequestDocumentation.parameterWithName(MobileSuitFactoryController.PARAM_MODEL_NAME).description("Mobile suit's model name"))));

    }

In the above code we use the RequestDocumentation.parameterWithName method to document such types of parameters.

Example of generated documentation:

Want to relax ?

We have already seen it with PayloadDocumentation.responseFields but it’s also true for other method described above, Spring Rest Docs ensures the strictness of your documentation. If you miss some part of your payload, or parameters tests will automatically fail.

If this behavior does not fit your needs, you can use the relaxed alternatives method like

  • PayloadDocumentation.relaxedRequestFields
  • PayloadDocumentation.relaxedResponseFields
  • RequestDocumentation.relaxedRequestParameters
  • RequestDocumentation.relaxedPathParameters

The signature and the use of these methods are generally the same as the strict ones.

Headers

Headers documentation snippet can also be generated.

Let’s add a new API endpoint for authentication. The tokens resource allow user to retrieve API authentication token through a basic authorization header.

@RestController
@RequestMapping(AuthController.API_ROOT_RESOURCE)
public class AuthController {

    public static final String API_ROOT_RESOURCE = "/auth";

    public static final String TOKEN_RESOURCE = "/token";

    public static final String AUTHORIZATION_HEADER = "Authorization";

    @GetMapping(TOKEN_RESOURCE)
    public String token(@RequestHeader(name = AUTHORIZATION_HEADER) String header) {
        Assert.notNull(header, "header is missing");
        return "123456";
    }
}

then we create the documentation test class

@RunWith(SpringRunner.class)
public class AuthControllerRestDocsTest {

    private MockMvc mockMvc;

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();

    @Before
    public void before() {
        mockMvc = MockMvcBuilders.standaloneSetup(new AuthController())
                .apply(MockMvcRestDocumentation.documentationConfiguration(this.restDocumentation))
                .build();
    }

    @Test
    public void tokens() throws Exception {
        mockMvc.perform(RestDocumentationRequestBuilders.get(AuthController.API_ROOT_RESOURCE + AuthController.TOKEN_RESOURCE)
                .accept(MediaType.APPLICATION_JSON).header(AuthController.AUTHORIZATION_HEADER, "Basic QWxhZGRpbjpPcGVuU2VzYW1l"))
                .andDo(MockMvcRestDocumentation.document("{ClassName}/{methodName}",
                        HeaderDocumentation.requestHeaders(
                                HeaderDocumentation.headerWithName(AuthController.AUTHORIZATION_HEADER).description("Basic Authorization header"))))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

We use the HeaderDocumentation factory class to document the header.

Running the test will generate a file named request-headers.snippet

Extras

In this part, I describe some useful hints and extra about Spring rest docs

Make your app serve your documentation

If you want your app to serve your documentation to the users, you need a litte of extra configuration:

Add the following plugin AFTER the ascii-doctor plugin configuration in your pom.xml.

maven-resources-plugin 2.7 copy-resources prepare-package copy-resources ${project.build.outputDirectory}/static/docs ${project.build.directory}/generated-docs

The plugin will copy the generated doc to the /static/docs in build output which then will be included in the jar file.

then simply add a resource handler in your Spring MVC config to serve the resource:

@Configuration
public class WebappConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/docs/**").addResourceLocations("classpath:/static/docs/");
    }
}

the documentation should be available at http://localhost:8080/docs/api-doc.html

Customize snippet

Snippet’s mustache template can be overriden.

Create the following directory structure in your test resources :

org/springframework/restdocs/templates/asciidoctor

For instance, you can override the request-fields snippet by adding request-fields.snippet into this package.

Example of request-fields.snippet content :

.{{title}}
|===
|Path|Type|Description|Constraints

{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{constraints}}

{{/fields}}
|===

It had a constraint field from FieldDescriptor. But be careful, snippet are Mustache template which does not allow null values, so if you add a new fields to a template you must populate it with a value in all of your descriptions.

Add constraint translation

To translate default constraints description (from english). Create the following directory structure:

/src/test/resources/org/springframework/restdocs/constraints

and add the bundle resource file according to your locale, for instance a file named ConstraintDescriptions_fr.FR.properties

Add your constraint description property in the file

Example:

javax.validation.constraints.NotNull.description=Ne doit pas être null

Thanks you for reading !

Sources are available at: https://github.com/slem1/spring-rest-docs-sample

Comments