Develop Spring Boot REST API with Open API 3.0
In this blog post we are going to explore how to develop REST API in Spring Boot framework based on the Open API 3.0 specification.
When you develop your REST API, unless you document it, it is not possible to know about available API, request and response format of API. One way to overcome this problem is to use documentation libraries like SpringDoc OpenAPI or Swagger Documentation libraries .While documentation can help humans, it can’t be used by the automated tools and they are dependent on language being used to develop REST API.
A Company called SmartBear developed Open API specification ( also called Swagger ) to define REST API in language-agnostic manner.The OpenAPI Specification was donated to the Linux Foundation under the OpenAPI Initiative in 2015.
Open API 3.0, formerly known as Swagger, is a specification for describing, producing, consuming, and visualizing REST APIs.
The goal of Open API 3.0 is to define a standard, language-agnostic interface to REST APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. In order to accomplish this, Open API 3.0 defines a number of terms and concepts which are necessary to understand in order to properly document a REST API. This includes such things as the available operations, the parameters which can be supplied to those operations, the format of the data which is returned, etc.
An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.
When developing REST API using Open API, you follow the below steps.
- First create Schema file as per your requirement using Open API specification.
- Use code generation tools to generate server code which contains data transfer object and interfaces with default implementation of API
- Write the implementation of API by extending the generated API interfaces
Creating Open API Schema
Let’s first create Open API schema. Schema contains license information, Component, APIs and Request and Response objects used by APIs.
Open API schema can be written either in json or yaml format.
The schema contains following elements
Datatypes supported by Open API
Following are the primitive data types supported by Specification
type | format | Comments |
---|---|---|
integer | int32 | signed 32 bits |
integer | int64 | signed 64 bits (a.k.a long) |
number | float | |
number | double | |
string | ||
string | byte | base64 encoded characters |
string | binary | any sequence of octets |
boolean | ||
string | date | |
string | date-time | |
string | password | A hint to UIs to obscure input. |
Some of complex datatypes supported by OAS are “object” and array” .
We can use primitive datatypes to define complex objects.
For example, below Employee object is defined using primitive datatypes and using $ref to refer the other object types and using enum to generate the enum data type.
"Employee" : {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"gender": {
"type": "string",
"enum": ["M","F"]
},
"birth_date": {
"type": "string",
"format": "date"
},
"hire_date": {
"type": "string",
"format": "date"
},
"Department" : {
"$ref": "#/components/schemas/Department"
}
}
},
Code language: Java (java)
Below example shows, how to define list of elements.
"Employees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Employee"
}
},
Code language: Java (java)
In below example shows how to refer the array of objects from other class.
"Department" : {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
},
"employees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Employee"
}
}
}
}
Code language: Java (java)
Paths element is used to define API end points.
"paths": {
"/employees": {
"get": {
"summary": "List all employees",
"operationId": "listEmployees",
"tags": [
"employees"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paged array of employees",
"headers": {
"x-next": {
"description": "A link to the next page of responses",
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Employees"
}
}
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
......
}
Code language: Java (java)
Server element is used to define the server endpoints
"servers": [
{
"url": "http://localhost:8080",
"description": "Development server"
},
{
"url": "https://test.server.com/v1",
"description": "Test server"
}
],
Code language: Java (java)
For more detailed information on Open API spec please visit the official documentation website.
Below is the sample REST API schema is based on Open API specification.
{
"openapi": "3.0.1",
"info": {
"title": "1.0",
"termsOfService": "http://example.com/terms/",
"contact": {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "[email protected]"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.1"
},
"servers": [
{
"url": "http://localhost:8080"
}
],
"components": {
"schemas": {
"ErrorResponse" : {
"type" : "object",
"properties" : {
"errors" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/ErrorModel"
}
}
}
},
"ErrorModel" : {
"type" : "object",
"properties" : {
"code" : {
"type" : "integer"
},
"message " : {
"type" : "string"
}
}
},
"Employee" : {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"gender": {
"type": "string",
"enum": ["M","F"]
},
"birth_date": {
"type": "string",
"format": "date"
},
"hire_date": {
"type": "string",
"format": "date"
},
"Department" : {
"$ref": "#/components/schemas/Department"
}
}
},
"Employees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Employee"
}
},
"Department" : {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
},
"employees": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Employee"
}
}
}
}
}
},
"paths": {
"/employees": {
"get": {
"summary": "List all employees",
"operationId": "listEmployees",
"tags": [
"employees"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paged array of employees",
"headers": {
"x-next": {
"description": "A link to the next page of responses",
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Employees"
}
}
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"post": {
"summary": "Create a Employee",
"operationId": "createEmployee",
"tags": [
"employees"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Employee"
}
}
}
},
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Employee"
}
}
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/employees/{id}": {
"get": {
"summary": "Info for a specific Employee",
"operationId": "getEmployeeById",
"tags": [
"employees"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The id of the Employee to retrieve",
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Employee"
}
}
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"delete": {
"summary": "Delete Employee By Id",
"operationId": "deleteEmployee",
"tags": [
"employees"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The id of the Employee to delete",
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Expected response to a valid request",
"content": {
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
}
}
}
Code language: Java (java)
components – contains schema objects which used to in request and response objects
paths – contains API end point which are exposed from the server
Developing CRUD example with Open API
Setting Up Project
Go to Spring Initializr website and add the following starter dependencies to a project.
- Web
- JPA
- Postgres
Add dependencies to Pom.xml
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.8</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.14.0</version>
</dependency>
Code language: Java (java)
Add OpenAPI Code Generator plugin to pom.xml
<build>
<plugins>
....
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.0.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>openapi.json</inputSpec>
<generatorName>spring</generatorName>
<output>${project.build.sourceDirectory}/generated-sources</output>
<additionalProperties>
<additionalProperty>useTag=true</additionalProperty>
</additionalProperties>
<configOptions>
<withXml>true</withXml>
<delegatePattern>true</delegatePattern>
<dateLibrary>java8</dateLibrary>
<interfaceOnly>true</interfaceOnly>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Code language: Java (java)
Now you can generate the DTO classes and API interfaces using below maven command.
mvn clean compile
Code language: Java (java)
If you look at the EmployeesAPI interface, it will contain default implementation for methods in API
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-11-26T00:53:16.090385300+13:00[Pacific/Auckland]")
@Validated
@Tag(name = "employees", description = "the employees API")
public interface EmployeesApi {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* POST /employees : Create a Employee
*
* @param employee (optional)
* @return Expected response to a valid request (status code 200)
* or unexpected error (status code 200)
*/
@Operation(
operationId = "createEmployee",
summary = "Create a Employee",
tags = { "employees" },
responses = {
@ApiResponse(responseCode = "200", description = "Expected response to a valid request", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = Employee.class))
}),
@ApiResponse(responseCode = "200", description = "unexpected error", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
})
}
)
@RequestMapping(
method = RequestMethod.POST,
value = "/employees",
produces = { "application/json" },
consumes = { "application/json" }
)
default ResponseEntity<Employee> _createEmployee(
@Parameter(name = "Employee", description = "") @Valid @RequestBody(required = false) Employee employee
) {
return createEmployee(employee);
}
.....
}
Code language: Java (java)
Now we write controller class and implement those methods.
@Controller
public class EmployeeController implements EmployeesApi {
public EmployeeService employeeService;
public EmployeeMapper employeeMapper;
public EmployeeController(EmployeeService employeeService,EmployeeMapper employeeMapper) {
this.employeeService = employeeService;
this.employeeMapper = employeeMapper;
}
public ResponseEntity<List<Employee>> listEmployees(Integer limit) {
List<Employee> empEntities = employeeMapper.toList(employeeService.getAllEmployees());
return new ResponseEntity(empEntities, HttpStatus.OK);
}
public ResponseEntity<Employee> createEmployee(Employee employee) {
Employee emp = employeeMapper.toDto(employeeService.createEmployee(employeeMapper.toEntity(employee)));
return new ResponseEntity(emp, HttpStatus.OK);
}
public ResponseEntity<Employee> getEmployeeById(Long id) {
Employee emp = employeeMapper.toDto(employeeService.getEmployeeById(id.intValue()));
return new ResponseEntity(emp, HttpStatus.OK);
}
public ResponseEntity<java.lang.Void> deleteEmployee(Long id) {
employeeService.deleteEmployee(id.intValue());
return new ResponseEntity( HttpStatus.OK);
}
}
Code language: Java (java)
When using Open API specification to develop Rest API, you need to write mapper class to transfer data between your entity classes and DTO classes generated by code generator as API return types are based on the generated DTO.
In this example I have used MapStruct library to map between entities and DTO’s.
The Service and Repository implementations will be similar to other CRUD example implementations.
You can download the complete code for this blog post from GitHub