Streamline API specifications with Swagger

on June 14, 2015

Share:

Recently we started using Swagger to improve the development of our APIs. The APIs will be used by other teams in our department. Proper documentation and tooling to help them consume these APIs is crucial. Swagger-codegen can generate several types of output if you supply it with a valid Swagger-specification in JSON format. Among these outputs are:

  • dynamic-html: well formatted documentation of your API specification
  • java: a client library that implements your API specification
  • spring-mvc: boiler-plate java server code
  • many other output types like nodejs, Ruby, etc..

In this post we will propose a new way of using Swagger to supercharge your Java webservice implementations; in particular using Spring-boot. It’s nice we can leverage Swagger for documentation and code generation. Any specification will change at some point in the future. So the big question is: How can cope with changing specifications? Especially during development time the specs change all the time. Regenerating the code and merging manually sounds like a nightmare. To address this issue we are proposing a novel way of using Swagger for generating Java code without getting in the way.

Current spring-mvc

The current implementation of spring-mvc code generator outputs a bare Spring-mvc server. It is a fully working package that can easily be compiled to a jar using maven. For our purposes there are a few shortcomings:

  • It assumes all methods return ResponseEntity instead of using the returnContainer specified by the user.
  • No support for overriding router-controller name. Swagger-tools supports the extension x-swagger-router-controller so that users can override the controllername of a particular operation. We will describe the issue in more detail later.
  • Doesn’t honour CLI-options supplied by the user to override package names etc. Thus all code is generated within the io.swagger namespace.

As mentioned earlier above points may not be an issue if you are going to change the generated code. However we aim to keep the generated code as read-only specification jar which our actual service implementation uses as dependency.

Example specification

Let’s consider the following hypothetical API specification:

{
    "swagger": "2.0",
    "info": {
        "title": "SampleService",
        "description": "My Service",
        "version": "1"
    },
    "produces": [
        "application/json"
    ],
    "host": "localhost:8080",
    "basePath": "/v1",
    "paths": {
        "/zone/{zone}/app/{app}": {
            "get": {
                "x-swagger-router-controller": "App",
                "operationId": "viewApp",
                "parameters": [
                    {"$ref": "#/parameters/ZoneParam"},
                    {"$ref": "#/parameters/AppParam"}
                ],
                "responses": {
                    "200": {
                        "description": "View app details",
                        "schema": {
                            "$ref": "#/definitions/App"
                        }
                    },
                    "default": {
                        "description": "Unexpected error",
                        "schema": {
                            "$ref": "#/definitions/Error"
                        }
                    }
                }
            }
        }
    },
    "definitions": {
        "App": {
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "Zone": {
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "Error": {
            "properties": {
                "code": {
                    "type": "integer",
                    "format": "int32"
                },
                "message": {
                    "type": "string"
                },
                "fields": {
                    "type": "string"
                }
            }
        }
    },
    "parameters": {
        "AppParam": {
            "name": "app",
            "in": "path",
            "type": "string"
        },
        "ZoneParam": {
            "name": "zone",
            "in": "path",
            "type": "string"
        }
    }
}

Please refer to the official Swagger-2.0 documentation for the details. There two remarks:

  • The spec was handwritten. Thus being able to write concisely is very important. We use the parameters and definitions sections alot to define frequently used clauses.
  • A defined path like /zone/{zone}/app/{app} can have one or more operations. The controller name of an operation is derived from the path by default. In this case the generator would produce ZoneApi class. Not exactly what we want since the viewApp operation is best placed in a controller called AppApi. To overcome this problem we use swagger extension x-swagger-router-controller to allow the user to override the controller name from the specification. For simple API definitions the default behavior is often sufficient. However for any decent size specs this just won’t cut it.

New workflow

We made some improvements in the spring-mvc codegenerator. The code is hosted at github and we will prepare a PullRequest soon for this to be included in the official codebase.

Let’s start by preparing swagger-codegen

# git clone https://github.com/xiwenc/swagger-codegen.git
# cd swagger-codegen
# git checkout spring-mvc
# mvn package

Copy our example spec to a file, let’s call it myservice.json. Also create another file myservice-config.json with content:

{
    "invokerPackage": "com.example.myservice.specs",
    "groupId": "com.example",
    "artifactId": "myservice-specs",
    "artifactVersion": "1.0.0"
}

We can generate the server code with:

# java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate -i myservice.json -l spring-mvc -o specs -c myservice-config.json
reading config from myservice-config.json
reading from myservice.json
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/model/App.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/model/Zone.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/model/Error.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/api/AppApi.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/api/AppApiTests.java
writing file /home/xcheng/git/swagger-codegen/specs/pom.xml
writing file /home/xcheng/git/swagger-codegen/specs/README.md
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/api/ApiException.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/api/ApiOriginFilter.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/api/ApiResponseMessage.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/api/NotFoundException.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/configuration/SwaggerConfig.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/configuration/WebApplication.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/configuration/WebMvcConfiguration.java
writing file /home/xcheng/git/swagger-codegen/specs/src/main/java/com/example/myservice/specs/configuration/SwaggerUiConfiguration.java

Finally we package it into a jar then install it to our local maven repository (You can install to remote also if you want but we won’t cover that in this article):

# cd specs
# mvn package
# mvn install:install-file -Dfile=target/myservice-specs-1.0.0.jar -DgroupId=com.example -DartifactId=myservice-specs -Dversion=1.0.0 -Dpackaging=jar

Import it into your maven project by adding the following to your pom.xml:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>myservice-specs</artifactId>
    <version>1.0.0</version>
</dependency>

API Implementation

The following snippet shows how the controller can be implemented by extending AppApi (generated) and overriding viewApp method.

package com.example.myservice;

import com.example.myservice.specs.model.App;
import com.example.myservice.specs.api.AppApi;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping(Application.APIV1 + "/zone/{zone}/app")
public class AppController extends AppApi {

    @Override
    @RequestMapping(value="/{app}", method=RequestMethod.GET)
    public App viewApp(@PathVariable("zone") String zone, @PathVariable("app") String appName) throws Exception {
        App app = new App();
        app.setName(appName);
        return app;
    }
}

Testing

As a bonus, we added generation of Test interfaces for each controller. The generated code located at src/main/java/com/example/myservice/specs/api/AppApiTests.java is:

package com.example.myservice.specs.api;

public interface AppApiTests {

  public void viewApp();


}

Your tests can then implement this contract. In a follow up article we will write how above service can be tested using Rest-Assurred with mocks.

Conclusions

The commits we made so far are:

  • 5709304 Honour CLI-options and introduce slightly better project structure
  • 39f047c feature(Tests) generate minimal interfaces for test classes
  • edc4c66 Throw generic Exception instead of NotFoundException
  • 417f227 Do not use ResponseEntity as container return type but use user-defined type instead
  • e6b45f2 Support controller name override with x-swagger-router-controller in the operation
  • 75962ff Generate returnContainer type instead of ResponseEntity if present

With our approach we radically changed how spring-mvc codegenerator is used. So worst case we will introduce a new generator based on current spring-mvc to accommodate users that favor editing generated source code. This would enable us to clean up a lot of the generated code and therefore improve readability.

# vim: ft=markdown:wrap

Subscribe to Our Blog

Receive Mendix platform tips, tricks, and other resources straight to your inbox every two weeks.

RSS Feed of the Mendix Blog
xcheng

About xcheng

I'm a Cloud Infrastructure Engineer at Mendix.