Janos Pasztor

Building an API-driven software: OpenAPI

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 = "Validation 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 ValidationErrror: " + 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.

The Swagger UI, displaying the automatically generated 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.

Janos Pasztor

I'm a DevOps engineer with a strong background in both backend development and operations, with a history of hosting and delivering content.

I run an active DevOps and development community on Discord, come in and say hi!

Join the community

Discord

Subscribe

Facebook Facebook Twitter Twitter GitHub GitHub
YouTube YouTube RSS Atom Feed
Do you want more? Click the buttons below!