This blog post can be divided into two parts: in the first part I will write a Spring Boot RESTful API and in the second part I'll show how to use JSONDoc to document the created API. Doing the two parts should take a maximum of 15 minutes since creating an API with Spring Boot is very easy and fast, and the same goes with documenting it with the JSONDoc Spring Boot starter and UI webjar. I'll skip tests creation for this example, since the main goal how to document the API rather than writing and testing it.
Let's begin with creating the Maven project based on the quickstart archetype
and declaring the needed dependencies for the API:
I also added Lombok to let me keep my code cleaner. The resulting pom looks like this:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jsondoc-shelf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jsondoc-shelf</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.176</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.14.8</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
This application will be a collection of services to manage a simple shelf. There will be two entities:
To do this I will create the usual components to manage the persistence and controller layers:
model
that will contain Book
and Author
repository
that will contain BookRepository
and AuthorRepository
controller
that will contain BookController
and AuthorController
For this example I will skip the service layer. I will also create a DatabasePopulator
class, implementing CommandLineRunner
so that at startup there will be some data in the in memory database. Let's see the entities, repositories and controllers code:
package org.example.shelf.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Entity
@Data
@EqualsAndHashCode(exclude = "id")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "title")
private String title;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
}
package org.example.shelf.model;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Data
@NoArgsConstructor
@ToString(exclude = "books")
@EqualsAndHashCode(of = "name")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "name")
private String name;
@JsonIgnore
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<Book>();
}
package org.example.shelf.repository;
import org.example.shelf.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
package org.example.shelf.repository;
import org.example.shelf.model.Author;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface AuthorRepository extends JpaRepository<Author, Long> {
}
package org.example.shelf.controller;
import java.util.List;
import org.example.shelf.flow.ShelfFlowConstants;
import org.example.shelf.model.Book;
import org.example.shelf.repository.BookRepository;
import org.jsondoc.core.annotation.Api;
import org.jsondoc.core.annotation.ApiBodyObject;
import org.jsondoc.core.annotation.ApiMethod;
import org.jsondoc.core.annotation.ApiPathParam;
import org.jsondoc.core.annotation.ApiResponseObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping(value = "/books", produces = MediaType.APPLICATION_JSON_VALUE)
public class BookController {
@Autowired
private BookRepository bookRepository;
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public Book findOne(@PathVariable Long id) {
return bookRepository.findOne(id);
}
@RequestMapping(method = RequestMethod.GET)
public List<Book> findAll() {
return bookRepository.findAll();
}
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public ResponseEntity<Void> save(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
bookRepository.save(book);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponentsBuilder.path("/books/{id}").buildAndExpand(book.getId()).toUri());
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void delete(@PathVariable Long id) {
Book book = bookRepository.findOne(id);
bookRepository.delete(book);
}
}
package org.example.shelf.controller;
import java.util.List;
import org.example.shelf.flow.ShelfFlowConstants;
import org.example.shelf.model.Author;
import org.example.shelf.repository.AuthorRepository;
import org.jsondoc.core.annotation.Api;
import org.jsondoc.core.annotation.ApiBodyObject;
import org.jsondoc.core.annotation.ApiMethod;
import org.jsondoc.core.annotation.ApiPathParam;
import org.jsondoc.core.annotation.ApiResponseObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping(value = "/authors", produces = MediaType.APPLICATION_JSON_VALUE)
public class AuthorController {
@Autowired
private AuthorRepository authorRepository;
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public Author findOne(@PathVariable Long id) {
return authorRepository.findOne(id);
}
@RequestMapping(method = RequestMethod.GET)
public List<Author> findAll() {
return authorRepository.findAll();
}
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public ResponseEntity<Void> save(@RequestBody Author author, UriComponentsBuilder uriComponentsBuilder) {
authorRepository.save(author);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponentsBuilder.path("/authors/{id}").buildAndExpand(author.getId()).toUri());
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void delete(@PathVariable Long id) {
Author author = authorRepository.findOne(id);
authorRepository.delete(author);
}
}
package org.example.shelf;
import org.example.shelf.model.Author;
import org.example.shelf.model.Book;
import org.example.shelf.repository.AuthorRepository;
import org.example.shelf.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DatabasePopulator implements CommandLineRunner {
@Autowired
private AuthorRepository authorRepository;
@Autowired
private BookRepository bookRepository;
public void run(String... arg0) throws Exception {
Author horbny = new Author();
horbny.setId(1L);
horbny.setName("Nick Horby");
Author smith = new Author();
smith.setId(2L);
smith.setName("Wilbur Smith");
authorRepository.save(horbny);
authorRepository.save(smith);
Book highFidelty = new Book();
highFidelty.setId(1L);
highFidelty.setTitle("High fidelty");
highFidelty.setAuthor(horbny);
Book aLongWayDown = new Book();
aLongWayDown.setId(2L);
aLongWayDown.setTitle("A long way down");
aLongWayDown.setAuthor(horbny);
Book desertGod = new Book();
desertGod.setId(3L);
desertGod.setTitle("Desert god");
desertGod.setAuthor(smith);
bookRepository.save(highFidelty);
bookRepository.save(aLongWayDown);
bookRepository.save(desertGod);
}
}
It's now time to write the main class to run the application. I'll call it Shelf
and also in this case, thanks to Spring Boot, it's very simple:
package org.example.shelf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableAutoConfiguration
@EnableJpaRepositories
@ComponentScan
public class Shelf {
public static void main(String[] args) {
SpringApplication.run(Shelf.class, args);
}
}
By running this class we can actually verify that the application is responding as expected to requests. You can easily test that the API work by using curl:
curl -i http://localhost:8080/books/1
curl -i http://localhost:8080/books
curl -i http://localhost:8080/authors/1
curl -i http://localhost:8080/authors
Here is the interesting and new part, i.e. using the JSONDoc library to annotate your code and automatically produce its documentation. To do that you have to declare the JSONDoc dependencies and insert a little code in your classes. Let's see how to do that:
Just add two more dependencies to the pom file:
<dependency>
<groupId>org.jsondoc</groupId>
<artifactId>spring-boot-starter-jsondoc</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.jsondoc</groupId>
<artifactId>jsondoc-ui-webjar</artifactId>
<version>1.1.3</version>
</dependency>
With the JSONDoc starter you can enable documentation generation just by adding @EnableJSONDoc
to the Shelf
class, which will be like:
package org.example.shelf;
import org.jsondoc.spring.boot.starter.EnableJSONDoc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableAutoConfiguration
@EnableJpaRepositories
@EnableJSONDoc
@ComponentScan
public class Shelf {
public static void main(String[] args) {
SpringApplication.run(Shelf.class, args);
}
}
Next thing to do is configure JSONDoc to scan your controllers, objects and flow classes. To do that just add some entries to the application.properties
file (create it under src/main/resources
if you don't have it)
jsondoc.version=1.0
jsondoc.basePath=http://localhost:8080
jsondoc.packages[0]=org.example.shelf.model
jsondoc.packages[1]=org.example.shelf.controller
JSONDoc can grab several information from the Spring annotations to build the documentation. Anyway it is an opt-in process, meaning that JSONDoc will scan classes and methods only if annotated with its own annotations. For example, to properly document the BookController
, here is how the JSONDoc annotations should be used:
package org.example.shelf.controller;
import java.util.List;
import org.example.shelf.flow.ShelfFlowConstants;
import org.example.shelf.model.Book;
import org.example.shelf.repository.BookRepository;
import org.jsondoc.core.annotation.Api;
import org.jsondoc.core.annotation.ApiBodyObject;
import org.jsondoc.core.annotation.ApiMethod;
import org.jsondoc.core.annotation.ApiPathParam;
import org.jsondoc.core.annotation.ApiResponseObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping(value = "/books", produces = MediaType.APPLICATION_JSON_VALUE)
@Api(description = "The books controller", name = "Books services")
public class BookController {
@Autowired
private BookRepository bookRepository;
@ApiMethod
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public @ApiResponseObject Book findOne(@ApiPathParam(name = "id") @PathVariable Long id) {
return bookRepository.findOne(id);
}
@ApiMethod
@RequestMapping(method = RequestMethod.GET)
public @ApiResponseObject List<Book> findAll() {
return bookRepository.findAll();
}
@ApiMethod
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public @ApiResponseObject ResponseEntity<Void> save(@ApiBodyObject @RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
bookRepository.save(book);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponentsBuilder.path("/books/{id}").buildAndExpand(book.getId()).toUri());
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
@ApiMethod
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void delete(@ApiPathParam(name = "id") @PathVariable Long id) {
Book book = bookRepository.findOne(id);
bookRepository.delete(book);
}
}
The same goes for AuthorController
.
Next thing to do is to put some JSONDoc annotations also on objects that need to be documented, in this case Book
and Author
. Here is the resulting Book
class:
package org.example.shelf.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import org.jsondoc.core.annotation.ApiObject;
import org.jsondoc.core.annotation.ApiObjectField;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Entity
@Data
@EqualsAndHashCode(exclude = "id")
@ApiObject
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@ApiObjectField(description = "The book's ID")
private Long id;
@Column(name = "title")
@ApiObjectField(description = "The book's title")
private String title;
@ManyToOne
@JoinColumn(name = "author_id")
@ApiObjectField(description = "The book's author")
private Author author;
}
And also in this case, the same goes for Author
.
Before going ahead with documenting flows, let's startup the application and see what happens:
http://localhost:8080/jsondoc
you will see a json, that is generated by JSONDoc and represents the documentation based on the annotation put on controllers methods and model objectshttp://localhost:8080/jsondoc-ui.html
you will see the JSONDoc UI. Just copy and paste http://localhost:8080/jsondoc
in the input field and get the documentation in the clear user interfaceThis is a good moment to take some time to explore the interface and play with the API in the playground.
By flow I mean a subsequential execution of a number of API methods aimed to achieve a goal, that could be purchase a book, or browse the catalog and get the book details. There are cases in which the flows could involve several methods, and API users could need to know which is the correct sequence of methods to call to achieve an objective. In this example I can't think of meaningful flows, but let's assume that I want to document the sequence of methods to browse the shelf and get an author's detail passing through a book I choose, so the resulting flow for this use case would be something like:
To document this flow you just need to follow these steps:
@ApiFlowSet
annotation, which makes JSONDoc understand that this class should be taken into account when building the documentation.@ApiFlow
. The body of the method, as well as its return type and argument can be void, since the method signature server only as a hook for the @ApiFlow
annotationfindAll
method of the BookController
can have an ID like BOOK_FIND_ALL
@ApiMethod
annotation and inside the apimethodid property of the @ApiFlowStep
annotationapplication.properties
file with that valueLet's see how I did it. Here is the class holding the flows of the application:
package org.example.shelf.flow;
import org.jsondoc.core.annotation.ApiFlow;
import org.jsondoc.core.annotation.ApiFlowSet;
import org.jsondoc.core.annotation.ApiFlowStep;
@ApiFlowSet
public class ShelfFlows {
@ApiFlow(
name = "Author detail flow",
description = "Gets an author's details starting from the book's list",
steps = {
@ApiFlowStep(apimethodid = ShelfFlowConstants.BOOK_FIND_ALL),
@ApiFlowStep(apimethodid = ShelfFlowConstants.BOOK_FIND_ONE),
@ApiFlowStep(apimethodid = ShelfFlowConstants.AUTHOR_FIND_ONE)
}
)
public void authorDetailFlow() {
}
}
Here is the class containing the methods IDs to be referenced in the annotations:
package org.example.shelf.flow;
public class ShelfFlowConstants {
// Book IDs
public final static String BOOK_FIND_ALL = "BOOK_FIND_ALL";
public final static String BOOK_FIND_ONE = "BOOK_FIND_ONE";
public final static String BOOK_SAVE = "BOOK_SAVE";
public final static String BOOK_DELETE = "BOOK_DELETE";
// Author IDs
public final static String AUTHOR_FIND_ALL = "AUTHOR_FIND_ALL";
public final static String AUTHOR_FIND_ONE = "AUTHOR_FIND_ONE";
public final static String AUTHOR_SAVE = "AUTHOR_SAVE";
public final static String AUTHOR_DELETE = "AUTHOR_DELETE";
}
Here is the resulting BookController
, after having specified the id property:
package org.example.shelf.controller;
import java.util.List;
import org.example.shelf.flow.ShelfFlowConstants;
import org.example.shelf.model.Book;
import org.example.shelf.repository.BookRepository;
import org.jsondoc.core.annotation.Api;
import org.jsondoc.core.annotation.ApiBodyObject;
import org.jsondoc.core.annotation.ApiMethod;
import org.jsondoc.core.annotation.ApiPathParam;
import org.jsondoc.core.annotation.ApiResponseObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping(value = "/books", produces = MediaType.APPLICATION_JSON_VALUE)
@Api(description = "The books controller", name = "Books services")
public class BookController {
@Autowired
private BookRepository bookRepository;
@ApiMethod(id = ShelfFlowConstants.BOOK_FIND_ONE)
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public @ApiResponseObject Book findOne(@ApiPathParam(name = "id") @PathVariable Long id) {
return bookRepository.findOne(id);
}
@ApiMethod(id = ShelfFlowConstants.BOOK_FIND_ALL)
@RequestMapping(method = RequestMethod.GET)
public @ApiResponseObject List<Book> findAll() {
return bookRepository.findAll();
}
@ApiMethod(id = ShelfFlowConstants.BOOK_SAVE)
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public @ApiResponseObject ResponseEntity<Void> save(@ApiBodyObject @RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
bookRepository.save(book);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponentsBuilder.path("/books/{id}").buildAndExpand(book.getId()).toUri());
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
@ApiMethod(id = ShelfFlowConstants.BOOK_DELETE)
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void delete(@ApiPathParam(name = "id") @PathVariable Long id) {
Book book = bookRepository.findOne(id);
bookRepository.delete(book);
}
}
And finally the application.properties
file, with the new package:
jsondoc.version=1.0
jsondoc.basePath=http://localhost:8080
jsondoc.packages[0]=org.example.shelf.model
jsondoc.packages[1]=org.example.shelf.controller
jsondoc.packages[2]=org.example.shelf.flow
It's time to startup the application again, go to http://localhost:8080/jsondoc-ui.html
, insert http://localhost:8080/jsondoc
in the input box and get the documentation. Enjoy!
Here is the resulting structure of the project: