Enterprise Integration Zone is brought to you in partnership with:

Matt Raible has been building web applications for most of his adult life. He started tinkering with the web before Netscape 1.0 was even released. For the last 16 years, Matt has helped companies adopt open source technologies (Spring, Hibernate, Apache, Struts, Tapestry, Grails) and use them effectively. Matt has been a speaker at many conferences worldwide, including Devoxx, Jfokus, ÜberConf, No Fluff Just Stuff, and a host of others. Matt is a DZone MVB and is not an employee of DZone and has posted 149 posts at DZone. You can read more from them at their website. View Full User Profile

Documenting your Spring API with Swagger

03.27.2014
| 20862 views |
  • submit to reddit

Over the last several months, I've been developing a REST API using Spring Boot. My client hired an outside company to develop a native iOS app, and my development team was responsible for developing its API. Our main task involved integrating with Epic, a popular software system used in Health care. We also developed a Crowd-backed authentication system, based loosely on Philip Sorst's Angular REST Security.

To document our API, we used Spring MVC integration for Swagger (a.k.a. swagger-springmvc). I briefly looked into swagger4spring-web, but gave up quickly when it didn't recognize Spring's @RestController. We started with swagger-springmvc 0.6.5 and found it fairly easy to integrate. Unfortunately, it didn't allow us to annotate our model objects and tell clients which fields were required. We were quite pleased when a new version (0.8.2) was released that supports Swagger 1.3 and its @ApiModelProperty.

What is Swagger?
The goal of Swagger 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.

To demonstrate how Swagger works, I integrated it into Josh Long's x-auth-security project. If you have a Boot-powered project, you should be able to use the same steps.

1. Add swagger-springmvc dependency to your project.

<dependency>
    <groupId>com.mangofactory</groupId>
    <artifactId>swagger-springmvc</artifactId>
    <version>0.8.2</version>
</dependency>

Note: on my client's project, we had to exclude "org.slf4j:slf4j-log4j12" and add "jackson-module-scala_2.10:2.3.1" as a dependency. I did not need to do either of these in this project.

2. Add a SwaggerConfig class to configure Swagger.

The swagger-springmvc documentation has an example of this with a bit more XML.

package example.config;

import com.mangofactory.swagger.configuration.JacksonScalaSupport;
import com.mangofactory.swagger.configuration.SpringSwaggerConfig;
import com.mangofactory.swagger.configuration.SpringSwaggerModelConfig;
import com.mangofactory.swagger.configuration.SwaggerGlobalSettings;
import com.mangofactory.swagger.core.DefaultSwaggerPathProvider;
import com.mangofactory.swagger.core.SwaggerApiResourceListing;
import com.mangofactory.swagger.core.SwaggerPathProvider;
import com.mangofactory.swagger.scanners.ApiListingReferenceScanner;
import com.wordnik.swagger.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.google.common.collect.Lists.newArrayList;

@Configuration
@ComponentScan(basePackages = "com.mangofactory.swagger")
public class SwaggerConfig {

    public static final List<String> DEFAULT_INCLUDE_PATTERNS = Arrays.asList("/news/.*");
    public static final String SWAGGER_GROUP = "mobile-api";

    @Value("${app.docs}")
    private String docsLocation;

    @Autowired
    private SpringSwaggerConfig springSwaggerConfig;
    @Autowired
    private SpringSwaggerModelConfig springSwaggerModelConfig;

    /**
     * Adds the jackson scala module to the MappingJackson2HttpMessageConverter registered with spring
     * Swagger core models are scala so we need to be able to convert to JSON
     * Also registers some custom serializers needed to transform swagger models to swagger-ui required json format
     */
    @Bean
    public JacksonScalaSupport jacksonScalaSupport() {
        JacksonScalaSupport jacksonScalaSupport = new JacksonScalaSupport();
        //Set to false to disable
        jacksonScalaSupport.setRegisterScalaModule(true);
        return jacksonScalaSupport;
    }


    /**
     * Global swagger settings
     */
    @Bean
    public SwaggerGlobalSettings swaggerGlobalSettings() {
        SwaggerGlobalSettings swaggerGlobalSettings = new SwaggerGlobalSettings();
        swaggerGlobalSettings.setGlobalResponseMessages(springSwaggerConfig.defaultResponseMessages());
        swaggerGlobalSettings.setIgnorableParameterTypes(springSwaggerConfig.defaultIgnorableParameterTypes());
        swaggerGlobalSettings.setParameterDataTypes(springSwaggerModelConfig.defaultParameterDataTypes());
        return swaggerGlobalSettings;
    }

    /**
     * API Info as it appears on the swagger-ui page
     */
    private ApiInfo apiInfo() {
        ApiInfo apiInfo = new ApiInfo(
                "News API",
                "Mobile applications and beyond!",
                "https://helloreverb.com/terms/",
                "matt@raibledesigns.com",
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0.html"
        );
        return apiInfo;
    }

    /**
     * Configure a SwaggerApiResourceListing for each swagger instance within your app. e.g. 1. private  2. external apis
     * Required to be a spring bean as spring will call the postConstruct method to bootstrap swagger scanning.
     *
     * @return
     */
    @Bean
    public SwaggerApiResourceListing swaggerApiResourceListing() {
        //The group name is important and should match the group set on ApiListingReferenceScanner
        //Note that swaggerCache() is by DefaultSwaggerController to serve the swagger json
        SwaggerApiResourceListing swaggerApiResourceListing = new SwaggerApiResourceListing(springSwaggerConfig.swaggerCache(), SWAGGER_GROUP);

        //Set the required swagger settings
        swaggerApiResourceListing.setSwaggerGlobalSettings(swaggerGlobalSettings());

        //Use a custom path provider or springSwaggerConfig.defaultSwaggerPathProvider()
        swaggerApiResourceListing.setSwaggerPathProvider(apiPathProvider());

        //Supply the API Info as it should appear on swagger-ui web page
        swaggerApiResourceListing.setApiInfo(apiInfo());

        //Global authorization - see the swagger documentation
        swaggerApiResourceListing.setAuthorizationTypes(authorizationTypes());

        //Every SwaggerApiResourceListing needs an ApiListingReferenceScanner to scan the spring request mappings
        swaggerApiResourceListing.setApiListingReferenceScanner(apiListingReferenceScanner());
        return swaggerApiResourceListing;
    }

    @Bean
    /**
     * The ApiListingReferenceScanner does most of the work.
     * Scans the appropriate spring RequestMappingHandlerMappings
     * Applies the correct absolute paths to the generated swagger resources
     */
    public ApiListingReferenceScanner apiListingReferenceScanner() {
        ApiListingReferenceScanner apiListingReferenceScanner = new ApiListingReferenceScanner();

        //Picks up all of the registered spring RequestMappingHandlerMappings for scanning
        apiListingReferenceScanner.setRequestMappingHandlerMapping(springSwaggerConfig.swaggerRequestMappingHandlerMappings());

        //Excludes any controllers with the supplied annotations
        apiListingReferenceScanner.setExcludeAnnotations(springSwaggerConfig.defaultExcludeAnnotations());

        //
        apiListingReferenceScanner.setResourceGroupingStrategy(springSwaggerConfig.defaultResourceGroupingStrategy());

        //Path provider used to generate the appropriate uri's
        apiListingReferenceScanner.setSwaggerPathProvider(apiPathProvider());

        //Must match the swagger group set on the SwaggerApiResourceListing
        apiListingReferenceScanner.setSwaggerGroup(SWAGGER_GROUP);

        //Only include paths that match the supplied regular expressions
        apiListingReferenceScanner.setIncludePatterns(DEFAULT_INCLUDE_PATTERNS);

        return apiListingReferenceScanner;
    }

    /**
     * Example of a custom path provider
     */
    @Bean
    public ApiPathProvider apiPathProvider() {
        ApiPathProvider apiPathProvider = new ApiPathProvider(docsLocation);
        apiPathProvider.setDefaultSwaggerPathProvider(springSwaggerConfig.defaultSwaggerPathProvider());
        return apiPathProvider;
    }


    private List<AuthorizationType> authorizationTypes() {
        ArrayList<AuthorizationType> authorizationTypes = new ArrayList<>();

        List<AuthorizationScope> authorizationScopeList = newArrayList();
        authorizationScopeList.add(new AuthorizationScope("global", "access all"));

        List<GrantType> grantTypes = newArrayList();

        LoginEndpoint loginEndpoint = new LoginEndpoint(apiPathProvider().getAppBasePath() + "/user/authenticate");
        grantTypes.add(new ImplicitGrant(loginEndpoint, "access_token"));

        return authorizationTypes;
    }

    @Bean
    public SwaggerPathProvider relativeSwaggerPathProvider() {
        return new ApiRelativeSwaggerPathProvider();
    }

    private class ApiRelativeSwaggerPathProvider extends DefaultSwaggerPathProvider {
        @Override
        public String getAppBasePath() {
            return "/";
        }

        @Override
        public String getSwaggerDocumentationBasePath() {
            return "/api-docs";
        }
    }
}

The ApiPathProvider class referenced above is as follows:

package example.config;

import com.mangofactory.swagger.core.SwaggerPathProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.ServletContext;

public class ApiPathProvider implements SwaggerPathProvider {
    private SwaggerPathProvider defaultSwaggerPathProvider;
    @Autowired
    private ServletContext servletContext;

    private String docsLocation;

    public ApiPathProvider(String docsLocation) {
        this.docsLocation = docsLocation;
    }

    @Override
    public String getApiResourcePrefix() {
        return defaultSwaggerPathProvider.getApiResourcePrefix();
    }

    public String getAppBasePath() {
        return UriComponentsBuilder
                .fromHttpUrl(docsLocation)
                .path(servletContext.getContextPath())
                .build()
                .toString();
    }

    @Override
    public String getSwaggerDocumentationBasePath() {
        return UriComponentsBuilder
                .fromHttpUrl(getAppBasePath())
                .pathSegment("api-docs/")
                .build()
                .toString();
    }

    @Override
    public String getRequestMappingEndpoint(String requestMappingPattern) {
        return defaultSwaggerPathProvider.getRequestMappingEndpoint(requestMappingPattern);
    }

    public void setDefaultSwaggerPathProvider(SwaggerPathProvider defaultSwaggerPathProvider) {
        this.defaultSwaggerPathProvider = defaultSwaggerPathProvider;
    }
}

In src/main/resources/application.properties, add an "app.docs" property. This will need to be changed as you move your application from local -> test -> staging -> production. Spring Boot's externalized configuration makes this fairly simple.

app.docs=http://localhost:8080

3. Verify Swagger produces JSON.

After completing the above steps, you should be able to see the JSON Swagger generates for your API. Open http://localhost:8080/api-docs in your browser or curl http://localhost:8080/api-docs.

{
    "apiVersion": "1",
    "swaggerVersion": "1.2",
    "apis": [
        {
            "path": "http://localhost:8080/api-docs/mobile-api/example_NewsController",
            "description": "example.NewsController"
        }
    ],
    "info": {
        "title": "News API",
        "description": "Mobile applications and beyond!",
        "termsOfServiceUrl": "https://helloreverb.com/terms/",
        "contact": "matt@raibledesigns.com",
        "license": "Apache 2.0",
        "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html"
    }
}

4. Copy Swagger UI into your project.

Swagger UI is a good-looking JavaScript client for Swagger's JSON. I integrated it using the following steps:

git clone https://github.com/wordnik/swagger-ui
cp -r swagger-ui/dist ~/dev/x-auth-security/src/main/resources/public/docs

I modified docs/index.html, deleting its header (<div id='header'>) element, as well as made its url dynamic.

...
$(function () {
  var apiUrl = window.location.protocol + "//" + window.location.host;
  if (window.location.pathname.indexOf('/api') > 0) {
    apiUrl += window.location.pathname.substring(0, window.location.pathname.indexOf('/api'))
  }
  apiUrl += "/api-docs";
  log('API URL: ' + apiUrl);
  window.swaggerUi = new SwaggerUi({
    url: apiUrl,
    dom_id: "swagger-ui-container",
...

After making these changes, I was able to open fire up the app with "mvn spring-boot:run" and view http://localhost:8080/docs/index.html in my browser.

5. Annotate your API.

There are two services in x-auth-security: one for authentication and one for news. To provide more information to the "news" service's documentation, add @Api and @ApiOperation annotations. These annotations aren't necessary to get a service to show up in Swagger UI, but if you don't specify the @Api("user"), you'll end up with an ugly-looking class name instead (e.g. example_xauth_UserXAuthTokenController).

@RestController
@Api(value = "news", description = "News API")
class NewsController {

    Map<Long, NewsEntry> entries = new ConcurrentHashMap<Long, NewsEntry>();

    @RequestMapping(value = "/news", method = RequestMethod.GET)
    @ApiOperation(value = "Get News", notes = "Returns news items")
    Collection<NewsEntry> entries() {
        return this.entries.values();
    }

    @RequestMapping(value = "/news/{id}", method = RequestMethod.DELETE)
    @ApiOperation(value = "Delete News item", notes = "Deletes news item by id")
    NewsEntry remove(@PathVariable Long id) {
        return this.entries.remove(id);
    }

    @RequestMapping(value = "/news/{id}", method = RequestMethod.GET)
    @ApiOperation(value = "Get a news item", notes = "Returns a news item")
    NewsEntry entry(@PathVariable Long id) {
        return this.entries.get(id);
    }

    @RequestMapping(value = "/news/{id}", method = RequestMethod.POST)
    @ApiOperation(value = "Update News", notes = "Updates a news item")
    NewsEntry update(@RequestBody NewsEntry news) {
        this.entries.put(news.getId(), news);
        return news;
    }
...
}

You might notice the screenshot above only shows news. This is because SwaggerConfig.DEFAULT_INCLUDE_PATTERNS only specifies news. The following will include all APIs.

public static final List<String> DEFAULT_INCLUDE_PATTERNS = Arrays.asList("/.*");

After adding these annotations and modifying SwaggerConfig, you should see all available services.

In swagger-springmvc 0.8.x, the ability to use @ApiModel and @ApiModelProperty annotations was added. This means you can annotate NewsEntry to specify which fields are required.

@ApiModel("News Entry")
public static class NewsEntry {
    @ApiModelProperty(value = "the id of the item", required = true)
    private long id;
    @ApiModelProperty(value = "content", required = true)
    private String content;

    // getters and setters
}

This results in the model's documentation showing up in Swagger UI. If "required" isn't specified, a property shows up as optional.

Parting Thoughts
The QA Engineers and 3rd Party iOS Developers have been very pleased with our API documentation. I believe this is largely due to Swagger and its nice-looking UI. The Swagger UI also provides an interface to test the endpoints by entering parameters (or JSON) into HTML forms and clicking buttons. This could benefit those QA folks that prefer using Selenium to test HTML (vs. raw REST endpoints).

I've been quite pleased with swagger-springmvc, so kudos to its developers. They've been very responsive in fixing issues I've reported. The only thing I'd like is support for recognizing JSR303 annotations (e.g. @NotNull) as required fields.

To see everything running locally, checkout my modified x-auth-security project on GitHub and the associated commits for this article.

Published at DZone with permission of Matt Raible, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Suman Jack replied on Thu, 2014/03/27 - 1:00pm

 Great article. Thanks for the detailed explanation.

I have a spring boot application with Jersey rest resources. I do not have @RequestMapping annotation for my resources. Instead I used @Path annotation and SpringServlet to expose the resources as JaxRs services. I tried the above swagger configuration but application fails to start with NPE in ApiListingScanner.scan.


Do you have an example for Spring boot+Jersey+Swagger with out spring MVC annotations?

Matt Raible replied on Thu, 2014/03/27 - 1:24pm in response to: Suman Jack

If you're using JAX-RS, you'll want to use a different dependency than swagger-springmvc. Here's a howto that might help:

https://github.com/wordnik/swagger-core/wiki/Java-JAXRS-Quickstart 

Suman Jack replied on Thu, 2014/03/27 - 4:16pm in response to: Matt Raible

 Thnx. I am having difficulties setting this up. Am a newbie to spring boot. Having few issues with configuration. I posted a question on stackoverflow . Can you please take a look?


Ben Hood replied on Wed, 2014/05/14 - 2:37pm

Hey Matt,

Many thanks for posting this simple example of how to get going - I was wondering whether there's a simple solution for the following missing dependency:

No qualifying bean of type [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] found for dependency [collection of org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

Was this implicitly (or explicitly) added to your config somewhere? 

Cheers,

Ben

Matt Raible replied on Tue, 2014/05/20 - 10:06am in response to: Ben Hood

Hello Ben,

I'm not sure what is causing this error for you. I'd suggest you compare your project with the x-auth-security project I modified. You can find it on GitHub at https://github.com/mraible/boot-examples/tree/master/x-auth-security.

Dilip Krishnan replied on Tue, 2014/06/17 - 9:56am in response to: Ben Hood

Ben,

I think you're probably missing the 

<context:annotation-config/>

or the java config equivalent in your spring configuration. Anyhow, 0.8.4 has been released and it should make your configuration much simpler!

-Dilip

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.