Replace EJson internals with avaje JsonMapper

Reworks the DJsonService (the SpiJsonService SPI behind io.ebean.text.json.EJson) to use avaje JsonMapper instead of the bespoke EJsonReader/EJsonWriter, consolidating all read/write logic into a single JsonAdapter.

Changes

 - New EbeanJsonAdapter — a JsonAdapter<Object> that materializes JSON into plain or modify-aware Map/List/Set, with two shared singletons (PLAIN, MODIFY_AWARE). Preserves existing EJson semantics: - Integral numbers → Long, decimals → BigDecimal
 - Write coverage for String/Integer/Long/Double/Float/BigDecimal/Boolean/Map/Collection with a toString() fallback for other types (UUID, enum, etc.)
 - Modify-aware loads share a single ModifyAwareFlag owner per root, reset to non-dirty once after the load completes
 - DJsonService now builds one JsonMapper + two JsonMapper.Type<Object> and routes parse/write through them. Null serialization is retained via a serializeNulls(true) writer; null/blank-input guards, token-honoring entry points, and parseSet (modify-aware asSet()) are preserved.
 - EJsonReader/EJsonWriter are now unused and should be deleted (couldn't remove them in this environment).

Why

Simplifies Ebean's JSON handling by reusing avaje-json-core's JsonMapper rather than maintaining a parallel reader/writer, while keeping behavior identical.
This commit is contained in:
robin.bygrave
2026-06-19 16:19:35 +12:00
parent 313fdda857
commit a8189567dd
4 changed files with 240 additions and 371 deletions
@@ -3,10 +3,13 @@ package io.ebeaninternal.json;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.mapper.JsonMapper;
import io.avaje.json.stream.JsonStream;
import io.ebean.service.SpiJsonService;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.LinkedHashSet;
@@ -16,98 +19,136 @@ import java.util.Set;
/**
* Utility that converts between JSON content and simple java Maps/Lists.
* <p>
* Backed by avaje {@link JsonMapper} using {@link EbeanJsonAdapter} which
* preserves Ebean's modify-aware collection and number semantics.
*/
public final class DJsonService implements SpiJsonService {
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private static final JsonMapper MAPPER = JsonMapper.builder().jsonStream(JSON_STREAM).build();
private static final JsonMapper.Type<Object> PLAIN = MAPPER.type(EbeanJsonAdapter.PLAIN);
private static final JsonMapper.Type<Object> MODIFY_AWARE = MAPPER.type(EbeanJsonAdapter.MODIFY_AWARE);
private static JsonMapper.Type<Object> type(boolean modifyAware) {
return modifyAware ? MODIFY_AWARE : PLAIN;
}
private static boolean blank(String content) {
return content == null || content.trim().isEmpty();
}
private static String readAll(Reader reader) throws IOException {
StringBuilder builder = new StringBuilder();
char[] buffer = new char[2048];
int len;
while ((len = reader.read(buffer)) != -1) {
builder.append(buffer, 0, len);
}
return builder.toString();
}
@Override
public String write(Object object) throws IOException {
return EJsonWriter.write(object);
StringWriter writer = new StringWriter();
write(object, writer);
return writer.toString();
}
@Override
public void write(Object object, Writer writer) throws IOException {
EJsonWriter.write(object, writer);
JsonWriter jsonWriter = JSON_STREAM.writer(writer);
jsonWriter.serializeNulls(true);
PLAIN.toJson(object, jsonWriter);
jsonWriter.flush();
}
@Override
public void write(Object object, JsonWriter jsonWriter) throws IOException {
EJsonWriter.write(object, jsonWriter);
PLAIN.toJson(object, jsonWriter);
}
@Override
public void writeCollection(Collection<Object> collection, JsonWriter jsonWriter) throws IOException {
EJsonWriter.writeCollection(collection, jsonWriter);
EbeanJsonAdapter.writeCollection(jsonWriter, collection);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(json, modifyAware);
return blank(json) ? null : (Map<String, Object>) type(modifyAware).fromJson(json);
}
@Override
public Map<String, Object> parseObject(String json) throws IOException {
return EJsonReader.parseObject(json);
return parseObject(json, false);
}
@Override
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(reader, modifyAware);
return parseObject(readAll(reader), modifyAware);
}
@Override
public Map<String, Object> parseObject(Reader reader) throws IOException {
return EJsonReader.parseObject(reader);
return parseObject(reader, false);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(JsonReader parser) throws IOException {
return EJsonReader.parseObject(parser);
return (Map<String, Object>) PLAIN.fromJson(parser);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
return EJsonReader.parseObject(parser, token);
return (Map<String, Object>) EbeanJsonAdapter.read(parser, token, false);
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseList(json, modifyAware);
return blank(json) ? null : (List<T>) type(modifyAware).fromJson(json);
}
@Override
@SuppressWarnings("unchecked")
public List<Object> parseList(String json) throws IOException {
return EJsonReader.parseList(json);
return (List<Object>) parseList(json, false);
}
@Override
public List<Object> parseList(Reader reader) throws IOException {
return EJsonReader.parseList(reader);
return parseList(readAll(reader));
}
@Override
@SuppressWarnings("unchecked")
public List<Object> parseList(JsonReader parser) throws IOException {
return EJsonReader.parseList(parser, false);
return (List<Object>) PLAIN.fromJson(parser);
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
return (List<T>) EJsonReader.parse(parser, currentToken, false);
return (List<T>) EbeanJsonAdapter.read(parser, currentToken, false);
}
@Override
public Object parse(String json) throws IOException {
return EJsonReader.parse(json);
return blank(json) ? null : PLAIN.fromJson(json);
}
@Override
public Object parse(Reader reader) throws IOException {
return EJsonReader.parse(reader);
return parse(readAll(reader));
}
@Override
public Object parse(JsonReader parser) throws IOException {
return EJsonReader.parse(parser);
return PLAIN.fromJson(parser);
}
@Override
@@ -1,204 +0,0 @@
package io.ebeaninternal.json;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.stream.JsonStream;
import io.ebean.ModifyAwareType;
import java.io.IOException;
import java.io.Reader;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
final class EJsonReader {
private EJsonReader() {
}
private static JsonReader reader(String content) {
return JsonStream.builder().build().reader(content);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json) throws IOException {
return (Map<String, Object>) parse(json);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader) throws IOException {
return (Map<String, Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(reader, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonReader parser) throws IOException {
return (Map<String, Object>) parse(parser);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
return (Map<String, Object>) parse(parser, token, false);
}
@SuppressWarnings("unchecked")
static <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return (List<T>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(String json) throws IOException {
return (List<Object>) parse(json);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(Reader reader) throws IOException {
return (List<Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(JsonReader parser, boolean modifyAware) throws IOException {
return (List<Object>) parse(parser, null, modifyAware);
}
static Object parse(String json) throws IOException {
return parseRawJson(json, null);
}
static Object parse(String json, boolean modifyAware) throws IOException {
return parseRawJson(json, modifyAware ? new ModifyAwareFlag() : null);
}
static Object parse(Reader reader) throws IOException {
return parseRawJson(readAll(reader), null);
}
static Object parse(Reader reader, boolean modifyAware) throws IOException {
return parseRawJson(readAll(reader), modifyAware ? new ModifyAwareFlag() : null);
}
static Object parse(JsonReader parser) throws IOException {
return parse(parser, null, false);
}
static Object parse(JsonReader parser, boolean modifyAware) throws IOException {
return parse(parser, null, modifyAware);
}
static Object parse(JsonReader parser, Token token, boolean modifyAware) throws IOException {
ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null;
Token effectiveToken = token == null ? parser.currentToken() : token;
if (effectiveToken == null) {
return parseRawJson(parser.readRaw(), owner);
}
return parseValue(parser, effectiveToken, owner);
}
private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) throws IOException {
if (token == null) {
token = parser.currentToken();
if (token == null) {
if (parser.isNullValue()) {
return null;
}
return parseRawJson(parser.readRaw(), owner);
}
}
if (token == Token.BEGIN_OBJECT) {
return parseObjectValue(parser, owner);
}
if (token == Token.BEGIN_ARRAY) {
return parseArrayValue(parser, owner);
}
if (token == Token.NUMBER) {
BigDecimal value = parser.readDecimal();
return value.scale() <= 0 ? value.longValue() : value;
}
if (token == Token.STRING) {
return parser.readString();
}
if (token == Token.BOOLEAN) {
return parser.readBoolean();
}
if (token == Token.NULL) {
parser.isNullValue();
return null;
}
return parseRawJson(parser.readRaw(), owner);
}
private static Object parseRawJson(String json, ModifyAwareType owner) throws IOException {
if (json == null) {
return null;
}
String content = json.trim();
if (content.isEmpty()) {
return null;
}
JsonReader parser = reader(content);
Token token = parser.currentToken();
return parseValue(parser, token, owner);
}
private static String readAll(Reader reader) throws IOException {
StringBuilder builder = new StringBuilder();
char[] buffer = new char[2048];
int len;
while ((len = reader.read(buffer)) != -1) {
builder.append(buffer, 0, len);
}
return builder.toString();
}
private static Map<String, Object> parseObjectValue(JsonReader parser, ModifyAwareType owner) throws IOException {
Map<String, Object> map = owner == null
? new LinkedHashMap<>()
: new ModifyAwareMap<>(owner, new LinkedHashMap<>());
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
map.put(fieldName, parseValue(parser, parser.currentToken(), owner));
if (owner != null) {
owner.setMarkedDirty(false);
}
}
parser.endObject();
if (owner != null) {
owner.setMarkedDirty(false);
}
return map;
}
private static List<Object> parseArrayValue(JsonReader parser, ModifyAwareType owner) throws IOException {
List<Object> list = owner == null
? new ArrayList<>()
: new ModifyAwareList<>(owner, new ArrayList<>());
parser.beginArray();
while (parser.hasNextElement()) {
Token elementToken = parser.currentToken();
Object elementValue = parseValue(parser, elementToken, owner);
list.add(elementValue);
if (owner != null) {
owner.setMarkedDirty(false);
}
}
parser.endArray();
if (owner != null) {
owner.setMarkedDirty(false);
}
return list;
}
}
@@ -1,149 +0,0 @@
package io.ebeaninternal.json;
import io.avaje.json.stream.JsonStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
/**
* Utility to write simple java Maps/Lists as JSON.
*/
final class EJsonWriter {
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private EJsonWriter() {
}
/**
* Convert object to Json content.
*/
static String write(Object object) throws IOException {
StringWriter writer = new StringWriter();
write(object, writer);
return writer.toString();
}
/**
* Convert object to Json content.
*/
static io.avaje.json.JsonWriter write(Object object, Writer writer) throws IOException {
io.avaje.json.JsonWriter jsonWriter = JSON_STREAM.writer(writer);
jsonWriter.serializeNulls(true);
write(object, jsonWriter);
jsonWriter.flush();
return jsonWriter;
}
/**
* Convert object to Json content.
*/
static void write(Object object, io.avaje.json.JsonWriter jsonWriter) throws IOException {
if (object == null) {
jsonWriter.nullValue();
return;
}
if (object instanceof String) {
jsonWriter.value((String) object);
return;
}
if (object instanceof Integer) {
jsonWriter.value((Integer) object);
return;
}
if (object instanceof Long) {
jsonWriter.value((Long) object);
return;
}
if (object instanceof Double) {
jsonWriter.value((Double) object);
return;
}
if (object instanceof Float) {
jsonWriter.value((Float) object);
return;
}
if (object instanceof BigDecimal) {
jsonWriter.value((BigDecimal) object);
return;
}
if (object instanceof Boolean) {
jsonWriter.value((Boolean) object);
return;
}
if (object instanceof Map<?, ?>) {
writeMap((Map<?, ?>) object, jsonWriter);
return;
}
if (object instanceof Collection<?>) {
writeCollection((Collection<?>) object, jsonWriter);
return;
}
jsonWriter.value(object.toString());
}
/**
* Write map as Json content.
*/
private static void writeMap(Map<?, ?> map, io.avaje.json.JsonWriter jsonWriter) throws IOException {
jsonWriter.beginObject();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String fieldName = (String) entry.getKey();
Object value = entry.getValue();
jsonWriter.name(fieldName);
write(value, jsonWriter);
}
jsonWriter.endObject();
}
/**
* Write list as Json content.
*/
static void writeCollection(Collection<?> list, io.avaje.json.JsonWriter jsonWriter) throws IOException {
jsonWriter.beginArray();
for (Object element : list) {
write(element, jsonWriter);
}
jsonWriter.endArray();
}
/**
* Convert map to Json content.
*/
static String write(Map<String, Object> map) throws IOException {
StringWriter writer = new StringWriter();
write(map, writer);
return writer.toString();
}
/**
* Convert map to Json content.
*/
static io.avaje.json.JsonWriter write(Map<String, Object> map, Writer writer) throws IOException {
io.avaje.json.JsonWriter jsonWriter = JSON_STREAM.writer(writer);
jsonWriter.serializeNulls(true);
write(map, jsonWriter);
jsonWriter.flush();
return jsonWriter;
}
/**
* Convert map to Json content.
*/
static void write(Map<String, Object> map, io.avaje.json.JsonWriter jsonWriter) throws IOException {
jsonWriter.beginObject();
Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Object> entry = it.next();
jsonWriter.name(entry.getKey());
write(entry.getValue(), jsonWriter);
}
jsonWriter.endObject();
}
}
@@ -0,0 +1,181 @@
package io.ebeaninternal.json;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.stream.JsonStream;
import io.ebean.ModifyAwareType;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Ebean specific {@link JsonAdapter} that materializes JSON into plain Java
* Map/List/scalar values - optionally wrapped in modify-aware collections so
* that mutations after load are tracked as dirty.
* <p>
* This consolidates the prior EJsonReader/EJsonWriter behavior into a single
* adapter that plugs into avaje {@code JsonMapper}.
*/
final class EbeanJsonAdapter implements JsonAdapter<Object> {
static final EbeanJsonAdapter PLAIN = new EbeanJsonAdapter(false);
static final EbeanJsonAdapter MODIFY_AWARE = new EbeanJsonAdapter(true);
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private final boolean modifyAware;
private EbeanJsonAdapter(boolean modifyAware) {
this.modifyAware = modifyAware;
}
@Override
public Object fromJson(JsonReader reader) {
return read(reader, null, modifyAware);
}
@Override
public void toJson(JsonWriter writer, Object value) {
write(writer, value);
}
/**
* Read a value honoring an explicitly supplied current token (or current token when null).
*/
static Object read(JsonReader parser, Token token, boolean modifyAware) {
ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null;
Token effectiveToken = token == null ? parser.currentToken() : token;
Object value;
if (effectiveToken == null) {
value = parseRawJson(parser.readRaw(), owner);
} else {
value = parseValue(parser, effectiveToken, owner);
}
if (owner != null) {
owner.setMarkedDirty(false);
}
return value;
}
private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) {
if (token == null) {
token = parser.currentToken();
if (token == null) {
if (parser.isNullValue()) {
return null;
}
return parseRawJson(parser.readRaw(), owner);
}
}
switch (token) {
case BEGIN_OBJECT:
return parseObjectValue(parser, owner);
case BEGIN_ARRAY:
return parseArrayValue(parser, owner);
case NUMBER:
BigDecimal value = parser.readDecimal();
return value.scale() <= 0 ? value.longValue() : value;
case STRING:
return parser.readString();
case BOOLEAN:
return parser.readBoolean();
case NULL:
parser.isNullValue();
return null;
default:
return parseRawJson(parser.readRaw(), owner);
}
}
private static Object parseRawJson(String json, ModifyAwareType owner) {
if (json == null) {
return null;
}
String content = json.trim();
if (content.isEmpty()) {
return null;
}
try (JsonReader parser = JSON_STREAM.reader(content)) {
return parseValue(parser, parser.currentToken(), owner);
}
}
private static Map<String, Object> parseObjectValue(JsonReader parser, ModifyAwareType owner) {
Map<String, Object> map = owner == null
? new LinkedHashMap<>()
: new ModifyAwareMap<>(owner, new LinkedHashMap<>());
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
map.put(fieldName, parseValue(parser, parser.currentToken(), owner));
}
parser.endObject();
return map;
}
private static List<Object> parseArrayValue(JsonReader parser, ModifyAwareType owner) {
List<Object> list = owner == null
? new ArrayList<>()
: new ModifyAwareList<>(owner, new ArrayList<>());
parser.beginArray();
while (parser.hasNextElement()) {
list.add(parseValue(parser, parser.currentToken(), owner));
}
parser.endArray();
return list;
}
/**
* Write the value to an existing JsonWriter (used for the raw stream paths).
*/
static void write(JsonWriter jsonWriter, Object object) {
if (object == null) {
jsonWriter.nullValue();
} else if (object instanceof String) {
jsonWriter.value((String) object);
} else if (object instanceof Integer) {
jsonWriter.value((Integer) object);
} else if (object instanceof Long) {
jsonWriter.value((Long) object);
} else if (object instanceof Double) {
jsonWriter.value((Double) object);
} else if (object instanceof Float) {
jsonWriter.value((Float) object);
} else if (object instanceof BigDecimal) {
jsonWriter.value((BigDecimal) object);
} else if (object instanceof Boolean) {
jsonWriter.value((Boolean) object);
} else if (object instanceof Map<?, ?>) {
writeMap(jsonWriter, (Map<?, ?>) object);
} else if (object instanceof Collection<?>) {
writeCollection(jsonWriter, (Collection<?>) object);
} else {
jsonWriter.value(object.toString());
}
}
private static void writeMap(JsonWriter jsonWriter, Map<?, ?> map) {
jsonWriter.beginObject();
for (Map.Entry<?, ?> entry : map.entrySet()) {
jsonWriter.name((String) entry.getKey());
write(jsonWriter, entry.getValue());
}
jsonWriter.endObject();
}
static void writeCollection(JsonWriter jsonWriter, Collection<?> collection) {
jsonWriter.beginArray();
for (Object element : collection) {
write(jsonWriter, element);
}
jsonWriter.endArray();
}
}