Linchakin

Event-Driven Architecture: Automatic DTO Generation From Event Documentation

 September 22, 2022     No comments   

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 the 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.

Problem

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:

  • you need to additionally parse the JSON document to extract objects that describe the components. Since jsonschema2pojo takes jsonschema objects as input, they are in the components block.
  • jsonschema2pojo does not work well with polymorphism and does not handle standard references from oneOf block that are in AsyncAPI. The description of inheritance requires special fields in the schema (extends.javaType), which cannot be added to the AsyncAPI documentation simply.
  • since the generated classes in our case should be used to deserialize messages from the broker, it is necessary to add Jackson annotations describing descriptors and subtypes.

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.

Documentation setup

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

}

Polymorphism

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

}

Plugin setup

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

Conclusion

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.

L O A D I N G
. . . comments & more!

Adblock test (Why?)


You may be interested in:
>> Is a Chromebook worth replacing a Windows laptop?
>> Find out in detail the outstanding features of Google Pixel 4a
>> Top 7 best earbuds you should not miss

Related Posts:
>> Recognizing 12 Basic Body Shapes To Choose Better Clothes
>>Ranking the 10 most used smart technology devices
>> Top 5+ Best E-readers: Compact & Convenient Pen
  • Share This:  
  •  Facebook
  •  Twitter
  •  Google+
  •  Stumble
  •  Digg
Email ThisBlogThis!Share to XShare to Facebook

Related Posts:

  • Blocs 4.4.0 – Visual web-design toolBlocs for Mac is a fast, easy-to-use, powerful visual web-design tool that lets you create beautiful, modern websites without the need to write code. … Read More
  • New Attack Let Hackers Steal Data From Air-Gapped Networks Using Ethernet Cable Internet is being used worldwide, and it is one of the most valuable assets in today’s generation. However, recently, it was being revealed that a da… Read More
  • Surface Pro Black Friday deals 2021: grab early savings on Surface Pro 7, 6 and XByBeren Neale HardwareOur guide to Surface Pro Black Friday deals, an...Surface Pro Black Friday deals are the perfect chance to get your hands on Microsoft's hybrid tablet at a discount price. And with Black Friday 2021 f… Read More
  • Diversify Your Digital Footprint Today! October 5th 2021 new story This is a lesson I learned the hard way, not once, not twice, but three times. I used to have a blog, … Read More
  • 11 OKR Examples for Product Managers Chasing Pirate Metrics October 5th 2021 new story The AARRR funnel (aka “Pirate Metrics”) is, in internet time at least, an age-old framework that divid… Read More
Newer Post Older Post Home

0 Comments:

Post a Comment


Copyright © 2025 Linchakin | Powered by Blogger
Design by Hardeep Asrani | Blogger Theme by NewBloggerThemes.com | Distributed By Gooyaabi Templates