Eliminating Bean–JSON Conversions by Backing DTOs with Vert.x JsonObject
Frequent bean-to-JSON and JSON-to-bean transformations introduce unnecessary CPU and GC overhead when Vert.x EventBus requires messages in io.vertx.core.json.JsonObject/JsonArray. Instead of serializing/deserializing on every hop, keep DTO state directly in a JsonObject and expose JavaBean-style accessors for framework compatibility. Shared utilities (date encoding/decoding, list bridging) live in a common base class.
Key points:
- Replace POJO fields with a JsonObject payload.
- Keep getters/setters so dependency injection and validation farmeworks still work.
- Centralize non-String handling (e.g., LocalDateTime/LocalDate) in the base class.
- Provide helpers to convert lists of DTOs to JsonArray and vice versa without materializing intermediate JSON strings.
Code: base class that stores data in JsonObject
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class JsonBackedDto {
private static final Logger LOG = LoggerFactory.getLogger(JsonBackedDto.class);
private static final DateTimeFormatter TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@JsonIgnore
protected JsonObject data;
public JsonBackedDto() {
this.data = new JsonObject();
}
public JsonBackedDto(JsonObject source) {
this.data = source == null ? new JsonObject() : source;
}
public JsonObject json() {
return data;
}
public void setJson(JsonObject source) {
this.data = source == null ? new JsonObject() : source;
}
// Date/time helpers
public LocalDateTime readDateTime(String key) {
String v = data.getString(key);
return v == null || v.isEmpty() ? null : LocalDateTime.parse(v, TS_FMT);
}
public LocalDate readDate(String key) {
String v = data.getString(key);
return v == null || v.isEmpty() ? null : LocalDate.parse(v, DATE_FMT);
}
public void writeDateTime(String key, LocalDateTime value) {
data.put(key, value == null ? null : value.format(TS_FMT));
}
public void writeDate(String key, LocalDate value) {
data.put(key, value == null ? null : value.format(DATE_FMT));
}
// Convenience conversions for lists of DTOs
public static <T extends JsonBackedDto> JsonArray toJsonArray(List<T> items) {
if (items == null) return null;
JsonArray arr = new JsonArray();
for (T it : items) {
arr.add(it.json());
}
return arr;
}
public static <T extends JsonBackedDto> List<T> fromJsonArray(JsonArray arr, Class<T> type) {
if (arr == null) return Collections.emptyList();
List<T> out = new ArrayList<>(arr.size());
for (Iterator<Object> it = arr.iterator(); it.hasNext();) {
Object next = it.next();
if (!(next instanceof JsonObject)) continue;
JsonObject jo = (JsonObject) next;
try {
T instance = type.getDeclaredConstructor().newInstance();
instance.setJson(jo);
out.add(instance);
} catch (ReflectiveOperationException e) {
LOG.error("Failed to instantiate {}", type.getName(), e);
}
}
return out;
}
}
Code: DTO implemanted on top of the base class
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import java.util.Collections;
import java.util.List;
public class CallNumbersDto extends JsonBackedDto {
public CallNumbersDto() {
super();
}
public CallNumbersDto(JsonObject source) {
super(source);
}
// Field keys
public static final String KEY_APP_ID = "appId";
public static final String KEY_NUMBERS = "nums";
public static final String KEY_PRODUCT_NAME = "productName";
public static final String KEY_PRIVACY = "privacy";
// Accessors expected by frameworks
public String getAppId() {
return json().getString(KEY_APP_ID);
}
public void setAppId(String value) {
json().put(KEY_APP_ID, value);
}
public List<String> getNums() {
JsonArray arr = json().getJsonArray(KEY_NUMBERS);
return arr == null ? Collections.emptyList() : arr.getList();
}
public void setNums(List<String> values) {
json().put(KEY_NUMBERS, values == null ? null : new JsonArray(values));
}
public String getProductName() {
return json().getString(KEY_PRODUCT_NAME);
}
public void setProductName(String value) {
json().put(KEY_PRODUCT_NAME, value);
}
public String getPrivacy() {
return json().getString(KEY_PRIVACY);
}
public void setPrivacy(String value) {
json().put(KEY_PRIVACY, value);
}
}
Typical high-frequency conversions that are no longer needed
// Previous approach with repeated (de)serialization
// JSONObject jo = (JSONObject) JSONObject.toJSON(fooBean);
// FooBean fooBean = (FooBean) JSONObject.toBean(jo, FooBean.class);
// New approach keeps everything in Vert.x types
CallNumbersDto dto = new CallNumbersDto();
dto.setAppId("app-1");
dto.setNums(List.of("10086", "10010"));
deployOnEventBus(dto.json()); // send JsonObject directly
Behavioral notes:
- All state lives in JsonObject; no duplication of fields.
- Getters/setters merely proxy to the underlying JsonObject, preserving compatibility with frameworks expecting JavaBeans.
- Date/time fields should be serialized using the base helpers to enforce cnosistent wire formats.
- When exchanging collections, use JsonBackedDto.toJsonArray and JsonBackedDto.fromJsonArray to convert between List<T> and JsonArray without intermediate string JSON.