One very important thing in the software development process that is often overlooked in the early stages of a project is API documentation. One of the solutions to this problem is frameworks for the automatic generation of documentation.
In the case of dividing the project into microservices and using Event-Driven architecture, the interaction between services is built using events transmitted through the message broker.
To generate documentation in the case of an Event-Driven architecture, there is AsyncApi. AsyncAPI is an open-source initiative that seeks to improve the current state of Event-Driven Architecture (EDA). AsyncApi has several Java tools that allow you to generate documentation from code. In this article, I described how to set up one of these springwolf tools.
In this article, I would like to tell you how I solved the following task, namely the generation of DTOs using the JSON documentation that springwolf generates.
The documentation structure that spring wolf generates looks like this:
{
"service": {
"serviceVersion": "2.0.0",
"info": {
//block with service info
},
"servers": {
"kafka": {
//describe of kafka connection
}
},
"channels": {
"kafka-channel": {
"subscribe": {
//...
"message": {
"oneOf": [
{
"name": "pckg.test.TestEvent",
"title": "TestEvent",
"payload": {
"$ref": "#/components/schemas/TestEvent"
}
}
]
}
},
//...
}
},
"components": {
"schemas": {
"TestEvent": {
//jsonschema of component
}
}
}
}
}
Since jsonschema is used to describe the components in the documentation, I decided to use the jsonschema2pojo library to solve this problem. However, in the process of trying to implement my plan, I ran into several problems:
All these problems led me to the need to implement my wrapper over jsonschema2pojo, which will extract the necessary information from the documentation, support polymorphism, and add Jackson annotations. The result is a Gradle plugin with which you can generate DTO classes for your project using the springwolf API. Next, I will try to demonstrate how to annotate classes for documentation and how to use the Springwolfdoc2dto plugin.
Here I would like to consider the specifics of when generation for non-primitive types such as Enum and Map. And also describe the necessary actions for polymorphism.
Let's look at the following message:
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TestEvent implements Serializable {
private String id;
private LocalDateTime occuredOn;
private TestEvent.ValueType valueType;
private Map<String, Boolean> flags;
private String value;
public enum ValueType {
STRING("STRING"),
BOOLEAN("BOOLEAN"),
INTEGER("INTEGER"),
DOUBLE("DOUBLE");
private final String value;
public ValueType(String value) {
this.value = value;
}
}
}
The jsonschema for such a message would look like this:
{
"service": {
//...
"components": {
"schemas": {
"TestEvent": {
"type": "object",
"properties": {
"id": {
"type": "string",
"exampleSetFlag": false
},
"occuredOn": {
"type": "string",
"format": "date-time",
"exampleSetFlag": false
},
"valueType": {
"type": "string",
"exampleSetFlag": false,
"enum": [
"STRING",
"BOOLEAN",
"INTEGER",
"DOUBLE"
]
},
"flags": {
"type": "object",
"additionalProperties": {
"type": "boolean",
"exampleSetFlag": false
},
"exampleSetFlag": false
},
"value": {
"type": "string",
"exampleSetFlag": false
}
},
"example": {
"id": "string",
"occuredOn": "2015-07-20T15:49:04",
"valueType": "STRING",
"flags": {
"additionalProp1": true,
"additionalProp2": true,
"additionalProp3": true
}
},
"exampleSetFlag": true
}
}
}
}
}
When generating DTO classes, we will get the following class structure. You can see that Enum is processed as in the original version, however, the collection of type Map<String, Boolean> has turned into a separate class Flags and the entire value of the collection itself will fall into the Flags.additionalProperties field.
package pckg.test;
// import
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"id",
"occuredOn",
"valueType",
"flags",
"value"
})
@Generated("jsonschema2pojo")
public class TestEvent implements Serializable
{
@JsonProperty("id")
private String id;
@JsonProperty("occuredOn")
private LocalDateTime occuredOn;
@JsonProperty("valueType")
private TestEvent.ValueType valueType;
@JsonProperty("flags")
private Flags flags;
@JsonProperty("value")
private String value;
@JsonIgnore
private Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>();
private final static long serialVersionUID = 7311052418845777748L;
// Getters ans Setters
@Generated("jsonschema2pojo")
public enum ValueType {
STRING("STRING"),
BOOLEAN("BOOLEAN"),
INTEGER("INTEGER"),
DOUBLE("DOUBLE");
private final String value;
private final static Map<String, TestEvent.ValueType> CONSTANTS = new HashMap<String, TestEvent.ValueType>();
static {
for (TestEvent.ValueType c: values()) {
CONSTANTS.put(c.value, c);
}
}
ValueType(String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
}
@JsonValue
public String value() {
return this.value;
}
@JsonCreator
public static TestEvent.ValueType fromValue(String value) {
TestEvent.ValueType constant = CONSTANTS.get(value);
if (constant == null) {
throw new IllegalArgumentException(value);
} else {
return constant;
}
}
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
})
@Generated("jsonschema2pojo")
public class Flags implements Serializable
{
@JsonIgnore
private Map<String, Boolean> additionalProperties = new LinkedHashMap<String, Boolean>();
private final static long serialVersionUID = 7471055390730117740L;
//getters and setters
}
And now let's look at how to provide a polymorphism option. This is relevant when we want to send several message subtypes to one broker topic and implement our listener for each subtype.
To do this, we need to add a parent class to the list of providers and add the @Schema annotation from swagger to it.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Setter(AccessLevel.PROTECTED)
@EqualsAndHashCode
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
visible = true,
defaultImpl = ChangedEvent.class
)
@JsonSubTypes(value = {
@JsonSubTypes.Type(name = ChangedEvent.type, value = ChangedEvent.class),
@JsonSubTypes.Type(name = DeletedEvent.type, value = DeletedEvent.class)
})
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(oneOf = {ChangedEvent.class, DeletedEvent.class},
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = ChangedEvent.type, schema = ChangedEvent.class),
@DiscriminatorMapping(value = DeletedEvent.type, schema = DeletedEvent.class),
})
public abstract class DomainEvent {
@Schema(required = true, nullable = false)
private String id;
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime occuredOn = LocalDateTime.now();
public abstract String getType();
}
/**
* Subtype ChangedEvent
*/
public class ChangedEvent
extends DomainEvent
implements Serializable
{
public static final String type = "CHANGED_EVENT";
private String valueId;
private String value;
}
/**
* Subtype DeletedEvent
*/
public class DeletedEvent
extends DomainEvent
implements Serializable
{
public static final String type = "DELETED_EVENT";
private String valueId;
}
In this case, the description of the components in the documentation will change as follows:
"components": {
"schemas": {
"ChangedEvent": {
"type": "object",
"properties": {
"id": {
"type": "string",
"exampleSetFlag": false
},
"occuredOn": {
"type": "string",
"format": "date-time",
"exampleSetFlag": false
},
"value": {
"type": "string",
"exampleSetFlag": false
},
"valueId": {
"type": "string",
"exampleSetFlag": false
},
"type": {
"type": "string",
"exampleSetFlag": false
}
},
"example": {
"id": "string",
"occuredOn": "2015-07-20T15:49:04",
"value": "string",
"valueId": "string",
"type": "CHANGED_EVENT"
},
"exampleSetFlag": true
},
"DeletedEvent": {
"type": "object",
"properties": {
"id": {
"type": "string",
"exampleSetFlag": false
},
"occuredOn": {
"type": "string",
"format": "date-time",
"exampleSetFlag": false
},
"valueId": {
"type": "string",
"exampleSetFlag": false
},
"type": {
"type": "string",
"exampleSetFlag": false
}
},
"example": {
"id": "string",
"occuredOn": "2015-07-20T15:49:04",
"valueId": "string",
"type": "DELETED_EVENT"
},
"exampleSetFlag": true
},
"DomainEvent": {
"type": "object",
"properties": {
"id": {
"type": "string",
"exampleSetFlag": false
},
"occuredOn": {
"type": "string",
"format": "date-time",
"exampleSetFlag": false
},
"type": {
"type": "string",
"exampleSetFlag": false
}
},
"example": {
"id": "string",
"occuredOn": "2015-07-20T15:49:04",
"type": "string"
},
"discriminator": {
"propertyName": "type",
"mapping": {
"CHANGED_EVENT": "#/components/schemas/ChangedEvent",
"DELETED_EVENT": "#/components/schemas/DeletedEvent"
}
},
"exampleSetFlag": true,
"oneOf": [
{
"$ref": "#/components/schemas/ChangedEvent",
"exampleSetFlag": false
},
{
"$ref": "#/components/schemas/DeletedEvent",
"exampleSetFlag": false
}
]
}
}
}
After that, the plugin will take into account the links from the oneOf block and the described discriminators. As a result, we get the following class structure.
package pckg.test;
// import
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"id",
"occuredOn",
"type"
})
@Generated("jsonschema2pojo")
@JsonTypeInfo(property = "type", use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(name = "CHANGED_EVENT", value = ChangedEvent.class),
@JsonSubTypes.Type(name = "DELETED_EVENT", value = DeletedEvent.class)
})
public class DomainEvent implements Serializable
{
@JsonProperty("id")
protected String id;
@JsonProperty("occuredOn")
protected LocalDateTime occuredOn;
@JsonProperty("type")
protected String type;
@JsonIgnore
protected Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>();
protected final static long serialVersionUID = 4691666114019791903L;
//getters and setters
}
// import
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"id",
"occuredOn",
"valueId",
"type"
})
@Generated("jsonschema2pojo")
public class DeletedEvent
extends DomainEvent
implements Serializable
{
@JsonProperty("id")
private String id;
@JsonProperty("occuredOn")
private LocalDateTime occuredOn;
@JsonProperty("valueId")
private String valueId;
@JsonProperty("type")
private String type;
@JsonIgnore
private Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>();
private final static long serialVersionUID = 7326381459761013337L;
// getters and setters
}
package pckg.test;
//import
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"id",
"occuredOn",
"value",
"type"
})
@Generated("jsonschema2pojo")
public class ChangedEvent
extends DomainEvent
implements Serializable
{
@JsonProperty("id")
private String id;
@JsonProperty("occuredOn")
private LocalDateTime occuredOn;
@JsonProperty("value")
private String value;
@JsonProperty("type")
private String type;
@JsonIgnore
private Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>();
private final static long serialVersionUID = 5446866391322866265L;
//getters and setters
}
To connect the plugin, you need to add it to the gradle.build file and specify the parameters:
folder was to generate DTO
package of new classes
springwolf documentation URL
the root name in the documentation, usually the name of the service
plugins {
id 'io.github.stepanovd.springwolf2dto' version '1.0.1-alpha'
}
springWolfDoc2DTO{
url = 'http://localhost:8080/springwolf/docs'
targetPackage = 'example.package'
documentationTitle = 'my-service'
targetDirectory = project.layout.getBuildDirectory().dir("generated-sources")
}
Run task using bash command:
./gradle -q generateDTO
In this article, I described how you can use the springwolfdocs2dto plugin to generate new DTO classes based on the AsyncApi documentation. At the same time, new classes will be according to original inheritance and contain Jackson annotations for correct deserialization. I hope you find this plugin useful to you.