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/", "[email protected]", "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": "[email protected]", "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.
Posted by Lars Fischer on March 28, 2014 at 12:48 PM MDT #
Posted by Greg Smith on April 03, 2014 at 09: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.
Posted by Pravin on July 29, 2014 at 06:46 PM MDT #
Posted by Matt Raible on July 29, 2014 at 06:50 PM MDT #
Many Thanks. It seems very interesting and looking forward to learn this technology.
Could you please provide githib link for Spring Security Swagger Project?
Thanks.
Posted by PA on April 02, 2015 at 08:56 PM MDT #
Posted by Matt Raible on April 02, 2015 at 08:59 PM MDT #
Posted by PA on April 03, 2015 at 03:29 PM MDT #
PA - I'd recommend looking at my boot-ionic project on GitHub. I updated it last September to show you can now use an @EnableSwagger annotation to configure Swagger. You can see it's dependencies here.
My article on Building a REST API with JAXB, Spring Boot and Spring Data might help too.
Posted by Matt Raible on April 03, 2015 at 03:39 PM MDT #
Hi I already have one project in Spring MVC and I am using only RestControllers for that, so as soon as I add below dependency it stops recognizing the RestController.
Any help??
This is what I am adding right now...
swagger-springmvc
Also i tried adding
jackson-module-scala_2.10
Please help.. I am not using Spring Boot. it is a simple Spring MVC project.
Posted by Diwakar on September 09, 2015 at 12:53 PM MDT #
Posted by Matt Raible on September 09, 2015 at 12:57 PM MDT #
Hello,
I follow all your steps, but still I don't see swagger UI looks working. When I execute my CURL http://localhost:8080/test-rest/api-docs command it gives me raw JSON (the same way you shown in your blog)
Also when I launch this URL http://localhost:8080/test-rest/docs/index.html, I see following error
Please suggest what is going on ?
Posted by Neha on January 18, 2016 at 05:42 PM MST #
Posted by Matt Raible on February 11, 2016 at 03:32 PM MST #
Posted by Sajin on February 17, 2016 at 06:27 AM MST #
Posted by Matt Raible on February 17, 2016 at 02:02 PM MST #