In our previous episode we discussed how to set up the database access with our Spring Boot app. Not it’s time to set up the whole OpenAPI stuff.
Source code
As previously, you can grab the source code of the previous article from GitHub, and the end result of this article as well.
How Swagger is implemented
Spring Boot itself does not contain support for Swagger (the toolkit that enables OpenAPI). Luckily, a third party library, Spring Fox, does fill that gap. Once it is configured we need to annotate our data models and controllers with the appropriate annotations to convey information about the API.
Dependencies
As a first step, we will add Spring Fox as a dependency:
<project>
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>
io.springfox
</groupId>
<artifactId>
springfox-swagger2
</artifactId>
<version>
2.9.2
</version>
</dependency>
</dependencies>
</project>
This will pull in a number of packages, including the Swagger annotations package, but not the UI, which we will add later. For now let’s just focus on getting the OpenAPI document up and running.
Configuring Swagger
In order to use Swagger we will need to create a configuration class in our project. This configuration class is automatically read and used by Spring Boot:
package at.pasztor.backend;
import com.fasterxml.classmate.TypeResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api(TypeResolver typeResolver) {
return new Docket(
DocumentationType.SWAGGER_2
)
.select()
.apis(
RequestHandlerSelectors
.withClassAnnotation(
RestController.class
)
)
.paths(
PathSelectors
.any()
)
.build()
.pathMapping("/")
.apiInfo(metadata())
.forCodeGeneration(true)
.useDefaultResponseMessages(false)
;
}
private static ApiInfo metadata() {
return new ApiInfoBuilder()
.title("pasztor.at API")
.description(
"This API exposes the pasztor.at functionality."
)
.version("1")
.build();
}
}
Let’s unpack this a bit. First of all, the @Configuration
annotation is the one that tells Spring Boot that this is
a configuration class and should be read. The @EnableSwagger2
annotation enables Swagger to work at all.
Going on, you can see that there is a method called api()
with an annotation called @Bean
. This annotation declares
that this method should be called whenever a Docket
object is needed for dependency injection.
The Docket
itself contains the basic details about the generated OpenAPI document. Most importantly, we declare that
only classes that have the RestController
annotation should be considered for the OpenAPI document.
If we now start our application and navigate to http://localhost:8080/v2/api-docs we will see a very detailed JSON output:
{
"swagger": "2.0",
"info": {
"description":
"This API exposes the pasztor.at functionality.",
"version": "1",
"title": "pasztor.at API"
},
"host": "localhost:8080",
"basePath": "/",
"tags": [
{
"name": "blog-post-controller",
"description": "Blog Post Controller"
}
],
"paths": {
"/posts": {
"get": {
//...
},
"post": {
//...
}
},
"/posts/{slug}": {
"get": {
//...
},
"delete": {
//...
},
"patch": {
//...
}
}
},
"definitions": {
//...
}
}
As you can see, Spring Fox mapped our existing APIs pretty well even without us giving it any additional information. This, of course, was also made possible because we coded it in a very clean fashion. However, some identifiers are not exactly ideal and having documentation would also be nice.
Adding API metadata
Let’s annotate our classes a little bit so we have a nice API documentation. Let’s start with the BlogPostController
and add an @API
annotation:
package at.pasztor.backend.post.api;
//...
@Api(
tags = "Blog posts"
)
@RestController
@RequestMapping("/posts")
public class BlogPostController {
//...
}
This simple annotation will add a proper tag name to the output, which will later show up nicely in the Swagger UI.
If you want, you can even add more details on the tags to the Docket
in the config by adding the following section:
return new Docket(DocumentationType.SWAGGER_2)
//...
.build()
.tags(
new Tag(
"Blog posts",
"Create, modify, delete and list blog posts"
)
)
This will add the additional description to the tag.
Next up, let’s annotate our endpoint methods like this:
@ApiOperation(
nickname = "create",
value = "Create a post",
notes = "Creates a blog post by providing its details."
)
@RequestMapping(
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public BlogPost create(
@ApiParam(required = true)
@RequestBody
BlogPostCreateRequest request
) {
//...
}
The @ApiOperation
annotation provides additional context to the operation. The nickname
parameter will be used
by code generation libraries to name the method, while the other parameters will be used for documentation purposes.
The @ApiParam
annotation on the request
variable declares, in this case, that the request is required. This will
not do any validation, but serve as code generation help, but more on code generation later. For now, let’s just
focus on getting our documentation clear.
Our next target for annotations is the BlogPostCreateRequest
object:
package at.pasztor.backend.post.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import javax.validation.constraints.Pattern;
public class BlogPostCreateRequest {
@ApiModelProperty(
required = true,
value = "URL slug",
notes = "URL part of this blog post.",
allowableValues = "range(1,255)",
position = 0
)
@Pattern(regexp = "[A-Za-z0-9\\-]+")
@JsonProperty(value = "slug", index = 0)
public final String slug;
@ApiModelProperty(
required = true,
value = "Title",
notes = "User-visible title of this post",
allowableValues = "range(1,255)",
position = 1
)
@JsonProperty(value = "title", index = 1)
public final String title;
@ApiModelProperty(
required = true,
value = "Content",
notes = "Content of this blog post",
allowableValues = "range(0,65535)",
position = 2,
allowEmptyValue = true
)
@JsonProperty(index = 2)
public final String content;
public BlogPostCreateRequest(
@JsonProperty("slug")
String slug,
@JsonProperty("title")
String title,
@JsonProperty("content")
String content
) {
this.slug = slug;
this.title = title;
this.content = content;
}
}
Now, this may seem strange. Why are the @ApiModelProperty
annotations on the properties and not on the constructor
where the JSON decoding actually takes place?
As it turns out, Spring Fox / Swagger takes its property definitions off of properties or methods, but not constructor
parameters. This is different to how @ApiParam
annotations are handled because in theory BlogPostCreateRequest
could
also be an object that is returned to the user.
You may also notice that the @ApiModelProperty
annotations contain quite some extra information. Most important to
us is the required
field and the allowableValues
field. The required
field is useful because the automatic client
code generators will use it to determine if the developer on the βother endβ has to do a null
check or
not.
The allowableValues
on the other hand will give the client some useful hints on what is accepted or not. It can take
two forms: either a comma-separated list of values, or the formats range[1,2]
or range(1,2)
. The former will act
like an enum, while the latter will determine the string length or acceptable number ranges for the input value.
Important! The @ApiModelProperty
annotation does not provide any validation, it only documents, unless you pull in and configure the springfox-bean-validators
package. However, my preferred way is different.
Finally, before we get to validation, let’s take a look at the entities. We can modify our BlogPost
entity as follows:
package at.pasztor.backend.post.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Pattern;
@Entity(name = "posts")
@Table(name = "posts")
public class BlogPost {
@Id
@Column(nullable = false)
@ApiModelProperty(
required = true,
value = "URL slug",
notes = "URL part of this blog post.",
allowableValues = "range(1,255)",
position = 0
)
@Pattern(regexp = "[A-Za-z0-9\\-]+")
@JsonProperty(required = true, index = 0)
public final String slug;
@ApiModelProperty(
required = true,
value = "Title",
notes = "User-visible title of this post",
allowableValues = "range(1,255)",
position = 1
)
@JsonProperty(required = true, index = 1)
@Column(nullable = false)
public final String title;
@ApiModelProperty(
required = true,
value = "Content",
notes = "Content of this blog post",
allowableValues = "range(0,65535)",
position = 2,
allowEmptyValue = true
)
@JsonProperty(required = true, index = 2)
@Column(nullable = false, length = 65535)
public final String content;
//...
}
Now, I know, this is getting pretty ugly, we’ll clean this up a bit more in this article, and we will split it in the next article.
Validation
Now, here comes the tricky part: we have, so far, documented what constraints our variables should abide by. However, this does not guarantee that it will be so. We will actually have to validate the input from our users.
This is very, very important. Just because we document it, does not mean it will be so, and a skilled attacker could submit, for example, a way too long content and fill up your disk or something similar.
So we need to validate the input that we receive. There most classic method is to simply add validation to our controller:
public class BlogPostController {
//...
@RequestMapping(
method = RequestMethod.POST,
consumes =
MediaType.APPLICATION_JSON_VALUE,
produces =
MediaType.APPLICATION_JSON_VALUE
)
public BlogPost create(
@ApiParam(required = true)
@RequestBody
BlogPostCreateRequest request
) {
if (request.slug.length() > 255) {
throw new Exception();
}
//More stuff like this
BlogPost post = new BlogPost(
request.slug,
request.title,
request.content
);
storage.store(post);
return post;
}
//...
}
Now, this is good, but let’s actually follow best practices and make sure our entity, our BlogPost
can never be
created in an invalid state:
public class BlogPost {
//...
public BlogPost(
String slug,
String title,
String content
) {
if (slug.length() > 255) {
throw new Exception();
}
//More stuff like this
this.slug = slug;
this.title = title;
this.content = content;
}
//...
}
This is a very defensive way of programming as there is no chance whatsoever that our BlogPost will be created with invalid data. However, I have one giant problem with this setup: we will be duplicating our rules since we need to document our rules for Swagger, and also implement them for validation.
Needless to say, adding a lot of if-else’s to the constructor would needlessly bloat the code of our entities and would not serve any real purpose at all. So, instead of writing all that code by hand, let’s create an interface:
package at.pasztor.backend.post.validation;
import at.pasztor.backend.post.exception.ApiException;
public interface EntityValidator<T> {
void validate(T entity) throws ApiException;
}
The <T>
designation is what’s called a generic. It does not refer to a specific type, but rather it is a type
placeholder. So with this interface done we can change out BlogPost
constructor to read as follows:
public class BlogPost {
//...
public BlogPost(
String slug,
String title,
String content,
EntityValidator<BlogPost> entityValidator
) throws ApiException {
this.slug = slug;
this.title = title;
this.content = content;
entityValidator.validate(this);
}
//...
}
In other words, every time we create a BlogPost
entity we will have to pass a validator. We could, of course, not
request a validator, but instead create an instance, but that would make our code much harder to maintain. The reason
for that is that that the implementation of the EntityValidator
may have further dependencies and our constructor
would end up being a mess.
As for the implementation of the EntityValidator
, we can pick any validation library we like. In my case I decided
to write my own, highly experimental library for this
purpose that reads my @ApiModelProperty
annotations and validates the entity based on those. The benefit of this
approach is that the validation remains part of your core business logic and is not some invisible part that is
provided by the framework, and it makes easier to write tests for the validation logic itself.
Having such a validator will allow us to throw an exception that contains the exact details of the errors, such as this:
{
"error": "VALIDATION_FAILED",
"errorMessage": "Please verify your input and correct the following errors.",
"validationErrors": {
"title": [
"minimum-length"
],
"slug": [
"minimum-length"
]
}
}
This will allow the frontend code to display an appropriate error message for each field. In order to facilitate that
we would need to change our ApiException
class to include those fields:
//...
public class ApiException extends Exception {
@JsonIgnore
public final HttpStatus status;
@ApiModelProperty(
required = true,
position = 0,
value = "Error code"
)
@JsonProperty(required = true, index = 0)
public final ErrorCode error;
@ApiModelProperty(
required = true,
position = 1,
value = "Error message"
)
@JsonProperty(required = true, index = 1)
public final String errorMessage;
@ApiModelProperty(
required = true,
position = 2,
value = "Validation errors",
notes = "Errors, keyed with the field"
)
@JsonProperty(required = true, index = 2)
public final Map<String, Set<ValidationError>>
validationErrors;
public ApiException(
HttpStatus status,
ErrorCode error,
String errorMessage,
Map<String, Set<ValidationError>>
validationErrors
) {
this.status = status;
this.error = error;
this.errorMessage = errorMessage;
this.validationErrors = validationErrors;
}
public enum ErrorCode {
VALIDATION_FAILED,
NOT_FOUND;
}
}
The ValidationError
itself is then an enum
as follows:
public enum ValidationError {
EXACT_VALUE("exact-value"),
DOMAIN_NAME_FORMAL("domain-name-formal"),
EMAIL("invalid-email"),
HTTP_URL("http-url"),
IN_LIST("in-list"),
INTEGER("integer"),
MAXIMUM_LENGTH("maximum-length"),
MAXIMUM("maximum"),
MINIMUM_LENGTH("minimum-length"),
MINIMUM("minimum"),
PATTERN("pattern"),
REQUIRED("required"),
SINGLE_LINE("single-line"),
UUID("uuid");
private final String key;
ValidationError(String key) {
this.key = key;
}
@JsonValue
public String toString() {
return this.key;
}
public static ValidationError fromString(
String value
) {
if (value == null) {
return null;
}
for (ValidationError v :
ValidationError.values()
) {
if (
v
.toString()
.equalsIgnoreCase(value)
) {
return v;
}
}
throw new InvalidParameterException(
"Invalid value for ValidationError: "
+ value
);
}
}
We are using an enum
to document what the possible validation errors are. While it is a bit uncomfortable to maintain,
but it will lead to a better documented API.
Now, you may notice that our application not only contains the @ApiModelProperty
, etc. annotations on our model
(BlogPost
), but also on our request objects such as BlogPostCreateRequest
. The validation library of your choice
may or may not automatically validate these objects, but be aware that if you throw an exception from the constructor
of these objects, Jackson (the JSON library) will catch that error and throw an error itself. That’s why many libraries,
such as mine, validate these objects automatically. In our case though that will have no bearing on the business logic
as it relies on the explicit call to validate the BlogPost
object, defending us against any invalid input.
It is also worth mentioning that, for some reason, JPA, the data persistence library, also validates entities and throws
errors if it encounters invalid objects. This, in and of itself should not be a problem, as the same validation rules
apply to the entity there as in the API endpoints. If you, for some reason, want to turn it off though, you can do so by
adding the following line to your application.properties
file:
spring.jpa.properties.javax.persistence.validation.mode=none
I won’t go into details how I implemented my validation library, or how that ties into the application, but you can
read the source code to get a better picture of how that’s done. Alternatively, you can add the @Valid
in the
BlogPostController
to the parameters:
@Valid
@RequestBody
BlogPostCreateRequest request
This will cause Spring Boot to automatically validate your inputs. You can also load the SpringFox Bean Validators in
your pom.xml
library to use your Swagger annotations:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>
springfox-bean-validators
</artifactId>
<version>2.9.2</version>
</dependency>
Swagger UI
OK, so validation check, but no API is cool enough without documentation. Thankfully, Swagger provides something called the Swagger UI. Thankfully, Spring Fox provides us with the ability to do that by simply adding the following dependency:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
Tadam, this will expose the Swagger UI at http://localhost:8080/swagger-ui.html
, rewarding us for all the hard work
we have put into the documentation.
If you want to implement more advanced things like changing this URL or deploying CSP headers you will have to do some more legwork, but that’s out of the scope of this post.
Next up
Now that we have our API basics set up, we want to increase our API discoverability so we’ll venture into the territory of HATEOAS and HAL. β