Documenting Spring REST APIs with Swagger OAS 3.0

by Horatiu Dan

Context

A while ago, I decided to opt for Swagger (UI) as a solution for documenting REST APIs. The APIs are built using Spring Framework (not Spring Boot), more exactly, Spring MVC. For the Swagger integration, I used the Springfox Swagger suite of java libraries.

The reasons of this integration are:

  • have a way of dynamically documenting the exposed REST APIs
  • have the documentation as close to code as possible so that the two are easily kept in sync and reviewed during code reviews
  • have the docs in standard form (OpenAPI Specification in this case) so that it can be further passed to other tools that accept the standard (e.g MuleSoft), if needed

According to Springfox Reference Documentation, these libraries allow “automating the generation of machine and human readable specifications for JSON APIs written using Spring projects”, thus Springfox OAS was chosen. Moreover, it conforms to OAS 3.0 (previously known as Swagger Specification).

In the beginning, I found confusing all these versions and acronyms, thus here it is how I would summarize it:

OpenAPI = Interface (specification)
Swagger = The tool(s) for implementing the interface (specification)

This article aims to provide guidelines on how to configure the integration of these products and put in practice the documentation aim. I find it useful especially because nowadays, there are less and less Spring web applications that are implemented without Spring Boot, for which the configuration is in my opinion slightly easier.

Set-up

  • Java 11
  • Maven 3.6.3
  • Spring Framework version 5.2.20.RELEASE
  • A web application that exposes a simple REST operation. A call to this operation shall provide a Bearer JWT in the ‘Authorization’ HTTP header. The Spring configuration is a mix of XML and annotations.

The components are briefly detailed below:

  • A simple GET operation that just echoes back a received parameter and that will be documented.
@RestController("v1EchoController")
public class EchoController {

    @GetMapping(value = "/v1/echo",
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> echo(@RequestParam String value) {
        return ResponseEntity.ok(value);
    }
}
  • Servlet mapping for the REST operation is configured in the web.xml file so that it is reachable at an URL of the following form /context-path/api/*.
<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>
		org.springframework.web.servlet.DispatcherServlet
	</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>	
	<url-pattern>/api/*</url-pattern>
</servlet-mapping>
  • A simple interceptor aimed to validate the ‘Authorization’ HTTP Header is configured as part of the web application context, in dispatcher-servlet.xml.
<mvc:interceptors>
	<mvc:interceptor>
	 	<mvc:mapping path="/v1/**"/> 
		<ref bean="tokenInterceptor" />
	</mvc:interceptor>
</mvc:interceptors>

The JWT verification is not among the goals of this article and thus it is delegated to a dedicated token manager.

@Component("tokenInterceptor")
public class TokenInterceptor extends HandlerInterceptorAdapter { 
	
	private final AuthTokenManager manager;
	
	@Autowired
	public TokenInterceptor(AuthTokenManager manager) {
		this.manager = manager;
	}
	
	@Override
	public boolean preHandle(HttpServletRequest request, 
			HttpServletResponse response, Object handler) {
			
		final String token = request.getHeader("Authorization");
		if (manager.isTokenValid(token)) {
			return true;
		}
		throw new UnauthorizedException("Invalid JWT.");
	}
}
  • A simple dedicated RutimeException that is used to signal the client call is not authorized
public class UnauthorizedException extends RuntimeException {

     public UnauthorizedException(String message) {
        super(message);
    }
}
  • A controller advice that handles the unauthorized situations in a decoupled way
@RestControllerAdvice(basePackageClasses = EchoController.class)
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {


    @ExceptionHandler(value = UnauthorizedException.class)
    protected ResponseEntity<?> handleUnauthorizedException(UnauthorizedException ex, 
			WebRequest request) {
			
        HttpHeaders headers = new HttpHeaders();
        headers.add("WWW-Authenticate", "User not authorized - provide a valid JWT.");

        return handleExceptionInternal(ex, HttpEntity.EMPTY.getBody(),
                headers, HttpStatus.UNAUTHORIZED, request);
    }
}

The components are straight forward, if the operation is invoked there are two possible situations:

  • without or with an invalid token
curl -X GET "http://localhost:8081/context-path/api/v1/echo?value=Test" -H "accept: application/json"

A 401 Unauthorized response is received whose header contains – www-authenticate: User is not authorized. Please provide a valid JWT – as configured.

  • with a valid the token
curl -X GET "http://localhost:8081/context-path/api/v1/echo?value=Horatiu" -H "accept: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzNjA5MzIiLCJpc3MiOiIiLCJhdWQiOiIiLCJleHAiOjE2NTE3NDU5NTh9.sIMtomF64RLaBbiwB0c41erhuWdD9AgOv3N6qdnrP8A"

A 200 OK response is received, whose body contains the sent parameter, as expected.

Steps for Documenting

In order to dynamically have an OAS 3.0 compliant documentation that is exposed in Swagger UI, the following steps should be accomplished.

  • Add Springfox Maven dependencies in pom.xml file
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-oas</artifactId>
	<version>3.0.0</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>3.0.0</version>
	<scope>runtime</scope>
</dependency>
  • Map Swagger UI in the web application context, by adding the following lines in dispatcher-servlet.xml file.
<mvc:resources mapping="/swagger-ui/**" 
	location="classpath:/META-INF/resources/webjars/springfox-swagger-ui/"/>
<mvc:resources mapping="/webjars/**" 
	location="classpath:/META-INF/resources/webjars/" />

A “link” towards springfox-swagger-ui-3.0.0.jar that contains the Swagger UI is now available.

  • Configure and Enable Swagger (UI)

Create a @Configuration class that contains the bean intended to be the primary interface into the Springfox framework – apiDocumentation().

@EnableOpenApi
@Configuration
public class SwaggerConfig {

	@Bean
	public Docket apiDocumentation() {
		return new Docket(DocumentationType.OAS_30)
			.apiInfo(apiInfo())
			.securityContexts(securityContexts())
			.securitySchemes(securitySchemes())
			.select()
			.apis(basePackage(EchoController.class.getPackage().getName()))
			.build()
			.genericModelSubstitutes(ResponseEntity.class)
			.globalResponses(HttpMethod.GET, globalResponses())
			.pathProvider(new CustomPathProvider());
	}

	private ApiInfo apiInfo() {
		return new ApiInfo("REST APIs", "Technically Correct APIs", "1.0",
			"urn:tos",
			new Contact("Technically Correct R&D", 
						"www.technically-correct.com", 
						"contact@technically-correct.com"),
			"License", "www.technically-correct.com/license",
			Collections.emptyList());
	}
	
	private List<SecurityContext> securityContexts() {
		SecurityReference securityReference = new SecurityReference(
			"bearerAuth", 
			new AuthorizationScope[0]);

		return List.of(SecurityContext.builder()
				.securityReferences(List.of(securityReference))
				.build());
	}

	private List<SecurityScheme> securitySchemes() {
		return List.of(HttpAuthenticationScheme.JWT_BEARER_BUILDER
				.name("bearerAuth")
				.description("Value shall contain a valid JWT.")
				.build());	
	}
	
	private List<Response> globalResponses() {
		List<Response> responses = new ArrayList<>();
		responses.add(new ResponseBuilder()
			.code(String.valueOf(HttpStatus.OK.value()))
			.description(HttpStatus.OK.getReasonPhrase() +
				" - The operation is successfully executed.")
			.build());
		responses.add(new ResponseBuilder()
			.code(String.valueOf(HttpStatus.UNAUTHORIZED.value()))
			.description(HttpStatus.UNAUTHORIZED.getReasonPhrase() +
				" - The client has not passed a valid JWT as 'Authorization' header.")
			.build());
		return responses;
	}
}

Some explanations are needed:

  • In order to enable the Swagger support, the configuration class shall be annotated with springfox.documentation.oas.annotations.EnableOpenApi.
  • For applying the security requirements described above into the Swagger UI, a security context and scheme are configured (securityContexts() and securitySchemes()).
  • For a client interacting with the REST API, the expected HTTP responses under different scenarios are very important, thus two global responses were defined – globalResponses(). These can be overwritten at operation level though.
  • For all documented operations, relative URLs are generated by default. If not specified otherwise, when constructing the Docket, a springfox.documentation.spring.web.paths.DefaultPathProvider is used. It implements the springfox.documentation.PathProvider interface and unfortunately defines the method for obtaining the operations paths in a literally hardcoded way.
protected String getDocumentationPath() {
    return "/";
}

This basically makes all documented operations be exposed in Swagger UI at URLs as /context-path/v1/ and not /context-path/api/v1 as needed in the case of this application. In order to overcome this, a CustomPathProvider was defined and such an instance was passed to the Docket – pathProvider(new CustomPathProvider()).

public class CustomPathProvider extends DefaultPathProvider {

	@Override
	public String getOperationPath(String operationPath) {
		String path = UriComponentsBuilder.fromPath("/")
				.path(operationPath).build().toString();

		if (!path.contains("/api/v1/")) {
			path = path.replace("/v1/", "/api/v1/");
		}
		return Paths.removeAdjacentForwardSlashes(path);
	}
}

Springfox version 2, had a method in the springfox.documentation.PathProvider interface that allowed customizing the paths values. Unfortunately, Springfox version 3.0 removed it and thus the need for such a custom path provider.

  • Annotate the REST operation using Springfox support to enrich and effectively document it
@ApiOperation(value = "Echoes back the sent value.")
@ApiResponses(value = {
		@ApiResponse(code = 200, message = "OK")
})
@GetMapping(value = "/v1/echo",
		produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> echo(@ApiParam(value = "The echoed value.") 
		@RequestParam String value) {
	return ResponseEntity.ok(value);
}

The Outcome

Swagger UI is accessible at http://localhost:8081/context-path/api/swagger-ui/index.html

Api-docs are available at http://localhost:8081/context-path/api/v3/api-docs in a JSON file that conforms to OAS 3.0 standard.

{
	"openapi": "3.0.3",
	"info": {
		"title": "REST APIs",
		"description": "Technically Correct APIs",
		"termsOfService": "urn:tos",
		"contact": {
			"name": "Technically Correct R&D",
			"url": "www.technically-correct.com",
			"email": "contact@technically-correct.com"
		},
		"license": {
			"name": "License",
			"url": "www.technically-correct.com/license"
		},
		"version": "1.0"
	},
	"servers": [
		{
			"url": "http://localhost:8081",
			"description": "Inferred Url"
		}
	],
	"tags": [
		{
			"name": "echo-controller",
			"description": "Echo Controller"
		}
	],
	"paths": {
		"/context-path/api/v1/echo": {
			"get": {
				"tags": [
					"echo-controller"
				],
				"summary": "Echoes back the sent value.",
				"operationId": "echoUsingGET",
				"parameters": [
					{
						"name": "value",
						"in": "query",
						"description": "Echoed back value.",
						"required": true,
						"style": "form",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "OK",
						"content": {
							"application/json": {
								"schema": {
									"type": "string"
								}
							}
						}
					},
					"401": {
						"description": "Unauthorized - The client has not passed a valid JWT as 'Authorization' header."
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				]
			}
		}
	},
	"components": {
		"securitySchemes": {
			"bearerAuth": {
				"type": "http",
				"description": "Value shall contain a valid JWT.",
				"scheme": "bearer",
				"bearerFormat": "JWT"
			}
		}
	}
}

The REST operation is documented in Swagger UI as expected.

Authorization token can be set using the Swagger UI Authorize button.

The operation may be invoked from Swagger UI.

Conclusions

The presented solution managed to offer a way of documenting REST APIs that are run in a non Spring Boot set-up. The main intent of the article is not the rich documentation in various scenarios, but the configuration and the integration with Springfox libraries.

From what was experienced, there are certain places (controllers, DTOs etc.) where io.swagger.v3.oas.annotations (v3) were mixed with io.swagger.annotations (v2) in order to overcome existing known bugs reported about Swagger Springfox dependencies. One such example is the fact that io.swagger.annotations.ApiModel and io.swagger.annotations.ApiModelProperty do not support model inheritance for the involved DTOs, thus io.swagger.v3.oas.annotations.media.Schema was used.

A next step would be to replace swagger-oas-ui with the more current springdoc-openapi-ui. This comes with additional modifications and configurations at all levels (dependency, config, annotations) and a good start would be the Springdoc migration guide – https://springdoc.org/migrating-from-springfox.html. As already stated, for non Spring Boot applications, the migration is definitely more challenging, but for sure solutions can be found.

References

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s