Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

Documenting your Spring API with Swagger

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.

Swagger UI News

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.

Swagger UI Complete

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.

Swagger UI Model

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.

Posted in Java at Mar 25 2014, 01:07:18 PM MDT 4 Comments
Comments:

Swagger is great, we also use it for the Magnolia REST API: http://documentation.magnolia-cms.com/display/DOCS/REST+API#RESTAPI-SwaggerAPIexplorer

Posted by Lars Fischer on March 28, 2014 at 06:48 AM MDT #

Hi Matt

Great stuff! I tried to use swagger4spring with Spring Boot with limited success. I did actually get it running but only with standard Spring Controllers as opposed to the RestController class. I also had to exclude the javax.servlet from the dependency tree (it was trying to pull in servlet 2.5 instead of servlet 3).

Thanks for the pointers on swagger-springmvc. I'll look into it.

Greg

Posted by Greg Smith on April 03, 2014 at 03:57 PM MDT #

Great article. I am using IBM Websphere 7.0 and while deploying spring application with swagger I get below classNotfoundexception. I think it has more to do with servlet version. Websphere 7.0 uses servlet 2.5 version. I m not sure how to get around with it.

Anyone's help will be greatly appreciated.

java.lang.ClassNotFoundException: class java.lang.InstantiationException: com.ibm.ws.webcontainer.servlet.SimpleFileServlet
	at java.beans.Beans.instantiate(Beans.java:190)
	at java.beans.Beans.instantiate(Beans.java:75)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper$1.run(ServletWrapper.java:1682)
	at com.ibm.ws.security.util.AccessController.doPrivileged(AccessController.java:118)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper.loadServlet(ServletWrapper.java:1673)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper.handleRequest(ServletWrapper.java:626)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper.handleRequest(ServletWrapper.java:443)
	at com.ibm.ws.webcontainer.servlet.ServletWrapperImpl.handleRequest(ServletWrapperImpl.java:175)
	at com.ibm.ws.webcontainer.webapp.WebAppRequestDispatcher.forward(WebAppRequestDispatcher.java:325)
	at org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler.handleRequest(DefaultServletHttpRequestHandler.java:122)
	at org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter.handle(HttpRequestHandlerAdapter.java:51)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:931)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:822)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:718)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:807)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:831)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper.service(ServletWrapper.java:1443)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper.handleRequest(ServletWrapper.java:790)
	at com.ibm.ws.webcontainer.servlet.ServletWrapper.handleRequest(ServletWrapper.java:443)
	at com.ibm.ws.webcontainer.servlet.ServletWrapperImpl.handleRequest(ServletWrapperImpl.java:175)
	at com.ibm.ws.webcontainer.webapp.WebApp.handleRequest(WebApp.java:3610)
	at com.ibm.ws.webcontainer.webapp.WebGroup.handleRequest(WebGroup.java:274)
	at com.ibm.ws.webcontainer.WebContainer.handleRequest(WebContainer.java:926)
	at com.ibm.ws.webcontainer.WSWebContainer.handleRequest(WSWebContainer.java:1557)
	at com.ibm.ws.webcontainer.channel.WCChannelLink.ready(WCChannelLink.java:173)
	at com.ibm.ws.http.channel.inbound.impl.HttpInboundLink.handleDiscrimination(HttpInboundLink.java:455)
	at com.ibm.ws.http.channel.inbound.impl.HttpInboundLink.handleNewInformation(HttpInboundLink.java:384)
	at com.ibm.ws.http.channel.inbound.impl.HttpInboundLink.ready(HttpInboundLink.java:272)
	at com.ibm.ws.tcp.channel.impl.NewConnectionInitialReadCallback.sendToDiscriminators(NewConnectionInitialReadCallback.java:214)
	at com.ibm.ws.tcp.channel.impl.NewConnectionInitialReadCallback.complete(NewConnectionInitialReadCallback.java:113)
	at com.ibm.ws.tcp.channel.impl.AioReadCompletionListener.futureCompleted(AioReadCompletionListener.java:165)
	at com.ibm.io.async.AbstractAsyncFuture.invokeCallback(AbstractAsyncFuture.java:217)
	at com.ibm.io.async.AsyncChannelFuture.fireCompletionActions(AsyncChannelFuture.java:161)
	at com.ibm.io.async.AsyncFuture.completed(AsyncFuture.java:138)
	at com.ibm.io.async.ResultHandler.complete(ResultHandler.java:202)
	at com.ibm.io.async.ResultHandler.runEventProcessingLoop(ResultHandler.java:766)
	at com.ibm.io.async.ResultHandler$2.run(ResultHandler.java:896)
	at com.ibm.ws.util.ThreadPool$Worker.run(ThreadPool.java:1527)

Posted by Pravin on July 29, 2014 at 12:46 PM MDT #

Pravin - it looks like swagger-springmvc:0.8.2 does build against Servlet 2.5. You might try asking this question on Stack Overflow.

Posted by Matt Raible on July 29, 2014 at 12:50 PM MDT #

Post a Comment:
  • HTML Syntax: Allowed