Documenting Spring Boot REST API with SpringDoc + OpenAPI 3

In previous tutorial, we are using SpringFox library to automate the documentation of our APIs. But even the latest version (SpringFox 2.9.2) still using version 2 of the OpenAPI Specification, and version 3 is not yet supported by SpringFox. In this tutorial, we will use another dependency for documenting REST API in OpenAPI version 3 format — springdoc-openapi.

Swagger vs OpenAPI

In short:

  • OpenAPI = Specification
  • Swagger = Tools for implementing the specification

The OpenAPI is the official name of the specification. The development of the specification is kickstarted in 2015 when SmartBear (the company that leads the development of the Swagger tools) donated the Swagger 2.0 specification to the Open API Initiative, a consortium of more the 30 organizations from different areas of the tech world. After this the specification was renamed to the OpenAPI Specification.

Since the Swagger tools were developed by the team involved in the creation of the original Swagger Specification, the tools are often still viewed as being synonymous with the spec. So Swagger still retain it's name for most well-known, and widely used tools for implementing the OpenAPI specification like Swagger Core, Swagger UI, and many more.

OpenAPI 3.0

End of July 2017, the OpenAPI Specification 3.0.0 was finally released by the Open API Initiative. It brings about a lot of improvements over the 2.0 specification. Open API 3.0 specifications can be written in JSON or YAML, and do an excellent job of documenting RESTful APIs.

springdoc-openapi

springdoc-openapi java library helps automating the generation of API documentation using spring boot projects. springdoc-openapi works by examining an application at runtime to infer API semantics based on spring configurations, class structure and various annotations.

You can add it as a dependency as the following in Maven:

<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.1.49</version> </dependency>

Let's use Spring Boot application generated before, and add following configuration:

OpenApiConfig.java
package com.dariawan.contactapp.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .components(new Components())
                .info(new Info().title("Contact Application API").description(
                        "This is a sample Spring Boot RESTful service using springdoc-openapi and OpenAPI 3."));
    }
}
                    

Now, let's change our controller, and add the documentation:

package com.dariawan.contactapp.controller;

...
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
...

@RestController
@RequestMapping("/api")
@Tag(name = "contact", description = "the Contact API")
public class ContactController {
    
    ...
    
    @Operation(summary = "Find Contacts by name", description = "Name search by %name% format", tags = { "contact" })
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "successful operation", 
                content = @Content(array = @ArraySchema(schema = @Schema(implementation = Contact.class)))) })	
    @GetMapping(value = "/contacts", produces = { "application/json", "application/xml" })
    public ResponseEntity<List<Contact>> findAll(
            @Parameter(description="Page number, default is 1") @RequestParam(value="page", defaultValue="1") int pageNumber,
            @Parameter(description="Name of the contact for search.") @RequestParam(required=false) String name) {
        ...
	return ...
    }

    @Operation(summary = "Find contact by ID", description = "Returns a single contact", tags = { "contact" })
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "successful operation",
                content = @Content(schema = @Schema(implementation = Contact.class))),
        @ApiResponse(responseCode = "404", description = "Contact not found") })
    @GetMapping(value = "/contacts/{contactId}", produces = { "application/json", "application/xml" })
    public ResponseEntity<Contact> findContactById(
            @Parameter(description="Id of the contact to be obtained. Cannot be empty.", required=true)
            @PathVariable long contactId) {
        ...
	return ...
    }
    
    @Operation(summary = "Add a new contact", description = "", tags = { "contact" })
    @ApiResponses(value = { 
        @ApiResponse(responseCode = "201", description = "Contact created",
                content = @Content(schema = @Schema(implementation = Contact.class))), 
        @ApiResponse(responseCode = "400", description = "Invalid input"), 
        @ApiResponse(responseCode = "409", description = "Contact already exists") })	
    @PostMapping(value = "/contacts", consumes = { "application/json", "application/xml" })
    public ResponseEntity<Contact> addContact(
            @Parameter(description="Contact to add. Cannot null or empty.", 
                    required=true, schema=@Schema(implementation = Contact.class))
            @Valid @RequestBody Contact contact) 
            throws URISyntaxException {
        
	...
        return ...
    }
    
    @Operation(summary = "Update an existing contact", description = "", tags = { "contact" })
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "successful operation"),
        @ApiResponse(responseCode = "400", description = "Invalid ID supplied"),
        @ApiResponse(responseCode = "404", description = "Contact not found"),
        @ApiResponse(responseCode = "405", description = "Validation exception") })
    @PutMapping(value = "/contacts/{contactId}", consumes = { "application/json", "application/xml" })
    public ResponseEntity<Void> updateContact(
            @Parameter(description="Id of the contact to be update. Cannot be empty.", 
                    required=true)
            @PathVariable long contactId,
            @Parameter(description="Contact to update. Cannot null or empty.", 
                    required=true, schema=@Schema(implementation = Contact.class))
            @Valid @RequestBody Contact contact) {
        ...
	return ...
    }
    
    @Operation(summary = "Update an existing contact's address", description = "", tags = { "contact" })
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "successful operation"),
        @ApiResponse(responseCode = "404", description = "Contact not found") })
    @PatchMapping("/contacts/{contactId}")
    public ResponseEntity<Void> updateAddress(
            @Parameter(description="Id of the contact to be update. Cannot be empty.",
                    required=true)
            @PathVariable long contactId,
            @Parameter(description="Contact's address to update.",
                    required=true, schema=@Schema(implementation = Address.class))
            @RequestBody Address address) {
        ...
	return ...
    }
    
    @Operation(summary = "Deletes a contact", description = "", tags = { "contact" })
    @ApiResponses(value = { 
        @ApiResponse(responseCode = "200", description = "successful operation"),
        @ApiResponse(responseCode = "404", description = "Contact not found") })
    @DeleteMapping(path="/contacts/{contactId}")
    public ResponseEntity<Void> deleteContactById(
            @Parameter(description="Id of the contact to be delete. Cannot be empty.",
                    required=true)
            @PathVariable long contactId) {
        ...
	return ...
    }
}
                    

Note: I'm not using @Parameter(name="...") property, because I find out sometimes I lost the schema in documentation. Not sure if this is a bug.

And changes on Contact and Address model, as example for Contact:

Contact.java
package com.dariawan.contactapp.domain;

import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Table(name = "contact")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
public class Contact implements Serializable {

    private static final long serialVersionUID = 4048798961366546485L;

    @Schema(description = "Unique identifier of the Contact.", 
            example = "1", required = true)
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    
    @Schema(description = "Name of the contact.", 
            example = "Jessica Abigail", required = true)
    @NotBlank
    @Size(max = 100)
    private String name;
    
    @Schema(description = "Phone number of the contact.", 
            example = "62482211", required = false)
    @Pattern(regexp ="^\\+?[0-9. ()-]{7,25}$", message = "Phone number")
    @Size(max = 25)
    private String phone;
    
    @Schema(description = "Email address of the contact.", 
            example = "jessica@ngilang.com", required = false)
    @Email(message = "Email Address")
    @Size(max = 100)
    private String email;
    
    @Schema(description = "Address line 1 of the contact.", 
            example = "888 Constantine Ave, #54", required = false)
    @Size(max = 50)
    private String address1;
    
    @Schema(description = "Address line 2 of the contact.", 
            example = "San Angeles", required = false)
    @Size(max = 50)
    private String address2;
    
    @Schema(description = "Address line 3 of the contact.", 
            example = "Florida", required = false)
    @Size(max = 50)
    private String address3;
    
    @Schema(description = "Postal code of the contact.", 
            example = "32106", required = false)
    @Size(max = 20)
    private String postalCode;
    
    @Schema(description = "Notes about the contact.", 
            example = "Meet her at Spring Boot Conference", required = false)
    @Column(length = 4000)
    private String note;    
}
                    

No change for the rest of project. Let's run our Spring Boot application and visit the URL http://localhost:8080/v3/api-docs

http://localhost:8080/v3/api-docs

OpenAPI v3.0 Docs

We can see that openapi metadata describing your API is already being generated, but for us is not very human readable. The good things is, springdoc-openapi-ui library automatically deploys swagger-ui to a spring-boot 2 application:

  • Documentation will be available in HTML format, using the official swagger-ui jars
  • The Swagger UI page should then be available at http://server:port/context-path/swagger-ui.html and the OpenAPI description will be available at the following url for json format: http://server:port/context-path/v3/api-docs
    • server: The server name or IP
    • port: The server port
    • context-path: The context path of the application

In our context, since our context path is /, then it will available in http://localhost:8080/swagger-ui.html (which will redirect to http://localhost:8080/swagger-ui/index.html?url=/v3/api-docs&validatorUrl=)

http://localhost:8080/swagger-ui/index.html?url=/v3/api-docs&validatorUrl=

OpenAPI v3.0 Docs - Swagger UI

And here Contact model as shown in swagger-ui.html:

http://localhost:8080/swagger-ui/index.html?url=/v3/api-docs&validatorUrl=

Contact Model

The documentation also available in yaml format as well, on following URL: http://localhost:8080/v3/api-docs.yaml. Here the yaml file generated (with some part purposely truncated):

openapi: 3.0.1 info: title: Contact Application API description: This is a sample Spring Boot RESTful service using springdoc-openapi and OpenAPI 3. servers: - url: http://localhost:8080 description: Generated server url tags: - name: contact description: the Contact API paths: /api/contacts/{contactId}: get: tags: - contact summary: Find contact by ID description: Returns a single contact operationId: findContactById parameters: - name: contactId in: path description: Id of the contact to be obtained. Cannot be empty. required: true schema: type: integer format: int64 responses: ... truncated ... 200: description: successful operation content: application/json: schema: $ref: '#/components/schemas/Contact' application/xml: schema: $ref: '#/components/schemas/Contact' put: tags: - contact summary: Update an existing contact operationId: updateContact parameters: - name: contactId in: path description: Id of the contact to be update. Cannot be empty. required: true schema: type: integer format: int64 requestBody: description: Contact to update. Cannot null or empty. content: application/json: schema: $ref: '#/components/schemas/Contact' application/xml: schema: $ref: '#/components/schemas/Contact' required: true responses: 404: description: Contact not found 405: description: Validation exception 400: description: Invalid ID supplied 200: description: successful operation delete: tags: - contact summary: Deletes a contact operationId: deleteContactById parameters: - name: contactId in: path description: Id of the contact to be delete. Cannot be empty. required: true schema: type: integer format: int64 responses: 404: description: Contact not found 200: description: successful operation patch: tags: - contact summary: Update an existing contact's address operationId: updateAddress parameters: - name: contactId in: path description: Id of the contact to be update. Cannot be empty. required: true schema: type: integer format: int64 requestBody: description: Contact's address to update. content: '*/*': schema: $ref: '#/components/schemas/Address' required: true responses: 404: description: Contact not found 200: description: successful operation /api/contacts: get: tags: - contact summary: Find Contacts by name description: Name search by %name% format operationId: findAll parameters: - name: page in: query description: Page number, default is 1 required: false schema: type: integer format: int32 default: 1 - name: name in: query description: Name of the contact for search. required: false schema: type: string responses: 200: description: successful operation content: application/json: schema: type: array items: $ref: '#/components/schemas/Contact' application/xml: schema: type: array items: $ref: '#/components/schemas/Contact' post: tags: - contact summary: Add a new contact operationId: addContact requestBody: description: Contact to add. Cannot null or empty. content: application/json: schema: $ref: '#/components/schemas/Contact' application/xml: schema: $ref: '#/components/schemas/Contact' required: true responses: 201: description: Contact created content: '*/*': schema: $ref: '#/components/schemas/Contact' ... truncated ... components: schemas: Contact: required: - id - name type: object properties: id: type: integer description: Unique identifier of the Contact. format: int64 example: 1 name: maxLength: 100 minLength: 0 type: string description: Name of the contact. example: Jessica Abigail phone: maxLength: 25 minLength: 0 pattern: ^\+?[0-9. ()-]{7,25}$ type: string description: Phone number of the contact. example: "62482211" email: maxLength: 100 minLength: 0 type: string description: Email address of the contact. example: jessica@ngilang.com address1: maxLength: 50 minLength: 0 type: string description: Address line 1 of the contact. example: "888" address2: maxLength: 50 minLength: 0 type: string description: Address line 2 of the contact. example: San Angeles address3: maxLength: 50 minLength: 0 type: string description: Address line 3 of the contact. example: Florida postalCode: maxLength: 20 minLength: 0 type: string description: Postal code of the contact. example: "32106" note: type: string description: Notes about the contact. example: Meet her at Spring Boot Conference Address: ... truncated ...

For more info about this dependency and related project, please visit https://springdoc.github.io/springdoc-openapi-demos/