Subscribe Us

Event-Driven Architecture: Automatic DTO Generation From Event Documentation

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.



Event-Driven Architecture: Automatic DTO Generation From Event Documentation
Source: Trends Pinoy

Post a Comment

0 Comments