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.

  1. First create Schema file as per your requirement using Open API specification.
  2. Use code generation tools to generate server code which contains data transfer object and interfaces with default implementation of API
  3. 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

Field NameTypeDescription
openapistringREQUIRED. This string MUST be the semantic version number of the OpenAPI Specification version that the OpenAPI document uses. The openapi field SHOULD be used by tooling specifications and clients to interpret the OpenAPI document.
infoInfo ObjectREQUIRED. Provides metadata about the API. The metadata MAY be used by tooling as required.
servers[Server Object]An array of Server Objects, which provide connectivity information to a target server. If the servers property is not provided, or is an empty array, the default value would be a Server Object with a url value of /.
pathsPaths ObjectREQUIRED. The available paths and operations for the API.
componentsComponents ObjectAn element to hold various schemas for the specification.
security[Security Requirement Object]A declaration of which security mechanisms can be used across the API. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. Individual operations can override this definition. To make security optional, an empty security requirement ({}) can be included in the array.
tags[Tag Object]A list of tags used by the specification with additional metadata. The order of the tags can be used to reflect on their order by the parsing tools. Not all tags that are used by the Operation Object must be declared. The tags that are not declared MAY be organized randomly or based on the tools’ logic. Each tag name in the list MUST be unique.
externalDocsExternal Documentation ObjectAdditional external documentation.

Datatypes supported by Open API

Following are the primitive data types supported by Specification

typeformatComments
integerint32signed 32 bits
integerint64signed 64 bits (a.k.a long)
numberfloat
numberdouble
string
stringbytebase64 encoded characters
stringbinaryany sequence of octets
boolean
stringdate
stringdate-time
stringpasswordA 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

Similar Posts