Make Swagger UI usable even with class inheritance on Spring Boot
Since the front end and back end developers can be normally working in different teams, the documentation of the API provided by the back end becomes more and more important for communication and integration.
For the back ends which are built by Spring Boot, the Swagger which is implemented by Springfox is the most popular way to provide the API documentation, because it not only renders a UI but also provides an API documentation in JSON format for code generation on client side.
In this article, I would like to share my “code first” experience with Swagger during implementing a REST API which data models have class inheritance while using Spring Boot.
Let me show you with an example project whose business functionality is to save and load the payment method of a contract.
Firstly I created the pom.xml to include following dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>json-inheritance</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>json-inheritance</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.10.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Then I had to defined a model PaymentInfo:
Where the AccountInfo is the super type of all payment methods like bank accout, PayPal or credit card etc. Because each payment method has different fields, this class is a pure placeholder.
The payment method via bank account was defined as following:
I created a controller like so:
After then I started the Spring Boot application.
But when I tried to open the Swagger-UI on 127.0.0.1:8080/swagger-ui.html I got the following error message:
To solve this problem I had to add the swagger configuration class:
Let’s try again to open the swagger UI. It works!
However, do you see the problem? The user has no idea what fields he shall fill into the account, even though the subtype is listed by the annotation @ApiModel on AccountInfo!
The solution was that I should add the two JsonTypeInfo annotations onto the AccountInfo:
Through that the definition of the BankAccount appeared on Swagger UI:
I think the reason might be that the Swagger uses by fasterxml.jackson by default.
Maybe the user could now have any idea about the data fields? Maybe.
To make this clearer, I annotated also on the controller end point:
However, not like I expected the description has not been shown on the UI.
It could be due to a bug of Springfox. I had to make the description elsewhere in @ApiOperation as workaround:
Eventually I got this after the end point name on the UI:
Let us try the POST request:
{
"account": {
"bankNumber":"1357",
"accountNumber":"2468"
},
"contractId": "1234"
}
Oops! I got an error:
{
"timestamp": "2019-07-22T14:18:23.097+0000",
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Missing type id when trying to resolve subtype of [simple type, type com.example.jsoninheritance.model.AccountInfo]: missing type id property 'type' (for POJO property 'account'); nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, type com.example.jsoninheritance.model.AccountInfo]: missing type id property 'type' (for POJO property 'account')\n at [Source: (PushbackInputStream); line: 5, column: 3] (through reference chain: com.example.jsoninheritance.model.PaymentInfo[\"account\"])",
"path": "/api/v1/paymentinfos"
}
If I paid attention to the lines
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({ @JsonSubTypes.Type(value = BankAccount.class, name = "com.example.model.BankAccount")})
I would not make this mistake.
The correct JSON request body should be like this:
{
"account": {
"type":"com.example.model.BankAccount",
"bankNumber":"1357",
"accountNumber":"2468"
},
"contractId": "1234"
}
When I set a debug point on the line before the method returns, I saw the request body was deserialized correctly to the BankAccount.
Other Hints:
- The Springfox Swagger 2.9.2 has a bug that it generates the discriminator field (i.e. type) not only in the super-class but also in the sub-classes. It is of course no go in Java. My workaround was to modify the generated api-docs manually, but I had to give up the online code generation and put the API Json file in a local directory of my client side. You could also use OpenAPI 3.0 library, but the annotations of Swagger 2 would not work any longer. (Update 2020–09–23: The Springfox 3.0 does not solve this problem, either, even with the new annotations “@Schema” instead of “@ApiModel”. The inheritance hierarchy cannot be generated correctly, because the “allof” keyword seemed not to be recognized by the Springfox.)
- The Springfox Swagger UI 2.9.2 is the current version for this article. It contains still the problem I mentioned above that the @ApiImplicitParam is buggy. I hope Springfox 3.0 could solve this problem.
- The JsonSubType does not have to be full package name + class name. It could be any name. Including the package name could just make it context safe.
- If you use Jackson ObjectMapper to serialize the model type BankAccount, the field “type” will be included automatically.
- BTW, I prefer “code first” against “contract first” action, even the latter is more popular so far. Editing a huge YAML file to define the API lets me remember the XML-definition for the Java beans. As we already known, the XML-way has been discarded, the programmers more like the Java configuration classes. Similar with Swagger annotations to the Java classes, it is more handy to read and write.