Migrate from Gson and org.json to Jackson

Replaced Gson and org.json usage across the codebase with a unified Jackson-based implementation through a shared `Json` utility. Updated all JSON serialization and deserialization logic (`GsonJsonMapper`, HTTP client, task persistence, command handling, tests) to use Jackson's `ObjectMapper`. Removed the outdated dependencies from `pom.xml` and verified symmetry in payload serialization.
This commit is contained in:
anthorob committed 2026-09-25 08:18:43 -04:00
1 parent 05b4a2ab85
commit 62de32ecfc
18 files changed
+225 -113

No files matched your search

+5 -5
View File
@@ -217,7 +217,7 @@ graph LR
# Delivery Steps
### * Step 1: Introduce Jackson and the shared Json utility
### ✓ Step 1: Introduce Jackson and the shared Json utility
Jackson is an explicit dependency and a single shared `ObjectMapper` is available to the codebase.
- Add `com.fasterxml.jackson.core:jackson-databind` to `pom.xml`, pinned to the version already resolved via JDA (2.21.2), next to the existing `org.json`/`gson` entries (which stay for now so the build keeps compiling).
@@ -225,14 +225,14 @@ Jackson is an explicit dependency and a single shared `ObjectMapper` is availabl
- Have `readObject`/`readArray` wrap Jackson's checked `JsonProcessingException` in an unchecked exception so existing call-site signatures stay unchanged.
- Verify no shade-plugin relocation or filter rules in `pom.xml` reference `org.json` or `gson`.
### Step 2: Migrate the Gson call sites to Jackson
### ✓ Step 2: Migrate the Gson call sites to Jackson
Gson is no longer referenced anywhere in the source tree.
- Delete `http/server/GsonJsonMapper.java` and remove its registration from the Javalin configuration, relying on Javalin 7's built-in Jackson `JsonMapper` instead.
- Replace `new Gson().toJson(params)` at `http/WebModule.java:256` with `Json.write(params)`, reusing the shared mapper instead of allocating per request.
- Confirm Javalin request/response serialization still behaves identically for the existing endpoints.
### Step 3: Migrate the HTTP client and its consumers
### ✓ Step 3: Migrate the HTTP client and its consumers
`WebModule` returns Jackson nodes and its callers read them.
- Change `WebModule.getAsObject()` to return `ObjectNode` and `getAsArray()` to return `ArrayNode` (lines 133-143), parsing through `Json`.
@@ -240,7 +240,7 @@ Gson is no longer referenced anywhere in the source tree.
- Update `command/crypto/CryptoPriceCommand.java` to read the crypto API response via `JsonNode` accessors.
- Remove the now-unused `org.json` imports from these three files.
### Step 4: Migrate the task persistence layer
### ✓ Step 4: Migrate the task persistence layer
Tasks serialize and restore through Jackson while keeping the existing on-disk payload shape.
- Change `PersistableTask.restore(JSONObject)` to `restore(ObjectNode)`.
@@ -249,7 +249,7 @@ Tasks serialize and restore through Jackson while keeping the existing on-disk p
- Update `PersistableTaskRegistry` and `db/service/TaskService` to parse stored payload strings via `Json.readObject`.
- Confirm `tasks/reaction/EmojiReactionRoleTask` still compiles unchanged against the new `EmojiTask` contract.
### Step 5: Update tests and drop the old dependencies
### ✓ Step 5: Update tests and drop the old dependencies
`org.json` and Gson are removed from `pom.xml` and the full build passes.
- Rewrite payload construction in `PersistableTaskRegistryTest` and `EmojiReactionRoleTaskPersistenceTest` using `ObjectNode`.
+3 -8
View File
@@ -217,14 +217,9 @@
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250517</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.14.0</version>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.2</version>
</dependency>
<dependency>
@@ -5,13 +5,14 @@ package me.bobert.netneutrality.bootstrap;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.db.service.GuildServiceProvider;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.MusicService;
import me.bobert.netneutrality.music.MusicManager;
import me.bobert.netneutrality.music.TrackScheduler;
import org.json.JSONArray;
import org.json.JSONObject;
import me.bobert.netneutrality.utils.Json;
import java.io.File;
import java.io.FileWriter;
@@ -43,8 +44,8 @@ public class BotState {
Map<String, GuildService> services = guildServices.getServices();
JSONObject main = new JSONObject();
JSONArray guildsArray = new JSONArray();
ObjectNode main = Json.object();
ArrayNode guildsArray = Json.array();
for (MusicManager manager : musicService.getManagers()) {
TrackScheduler trackScheduler = manager.getScheduler();
@@ -53,8 +54,8 @@ public class BotState {
for (GuildService service : services.values()) {
JSONObject guild = new JSONObject();
JSONObject music = new JSONObject();
ObjectNode guild = Json.object();
ObjectNode music = Json.object();
guild.put("id", service.getEntity().getGuildId());
// MessageChannel currentChannel = trackScheduler.getTextChannel();
@@ -64,12 +65,12 @@ public class BotState {
// music.put("current_channel", currentChannel.getId());
// }
guild.put("music", music);
guild.set("music", music);
guildsArray.put(guild);
guildsArray.add(guild);
}
main.put("guilds", guildsArray);
main.set("guilds", guildsArray);
File state = new File("state.json");
try {
@@ -5,6 +5,8 @@ package me.bobert.netneutrality.command.crypto;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.core.LogHandler;
import me.bobert.netneutrality.core.command.CommandCategory;
import me.bobert.netneutrality.core.command.CommandExecutionResult;
@@ -15,7 +17,6 @@ import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.http.WebModule;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.json.JSONObject;
import java.text.NumberFormat;
import java.util.Currency;
@@ -91,14 +92,19 @@ public class CryptoPriceCommand extends AbstractCommand {
}
WebModule http = new WebModule.Builder(String.format(baseURL, symbol, finalCurrency)).cache(30, TimeUnit.MINUTES).build().request();
JSONObject json = http.getAsObject();
ObjectNode json = http.getAsObject();
if (json.isEmpty() || (json.has("Response") && json.getString("Response").equalsIgnoreCase("Error"))) {
if (json.isEmpty() || json.path("Response").asText("").equalsIgnoreCase("Error")) {
asyncMessage.edit("Crypto doesn't exist.. yet");
return;
}
double value = json.getDouble(finalCurrency);
JsonNode priceNode = json.path(finalCurrency);
if (!priceNode.isNumber()) {
throw new IllegalStateException("Missing price for currency " + finalCurrency);
}
double value = priceNode.asDouble();
String price = dollarFormat.format(value);
asyncMessage.edit(String.format("The current price of %s in %s is %s", symbol, finalCurrency, price));
@@ -115,12 +121,12 @@ public class CryptoPriceCommand extends AbstractCommand {
private boolean validateSymbol(String symbol) {
try {
WebModule http = new WebModule.Builder(exchangeApi).get().cache(1, TimeUnit.DAYS).build().request();
JSONObject json = http.getAsObject();
ObjectNode json = http.getAsObject();
JSONObject pairs = json.getJSONObject("Data")
.getJSONObject("exchanges")
.getJSONObject(exchange)
.getJSONObject("pairs");
JsonNode pairs = json.path("Data")
.path("exchanges")
.path(exchange)
.path("pairs");
return pairs.has(symbol);
@@ -5,8 +5,8 @@ package me.bobert.netneutrality.core.tasks;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.core.context.CommandContext;
import org.json.JSONObject;
/**
* Represents a task that can be persisted and restored later.
@@ -29,7 +29,7 @@ public interface PersistableTask extends Task {
*
* @param payload the persisted payload, must not be {@code null}
*/
void restore(JSONObject payload);
void restore(ObjectNode payload);
void insert(CommandContext context);
@@ -5,10 +5,11 @@ package me.bobert.netneutrality.core.tasks.impl;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.tasks.PersistableTask;
import me.bobert.netneutrality.core.tasks.TaskManager;
import org.json.JSONObject;
import me.bobert.netneutrality.utils.Json;
public abstract class AbstractPersistableTask extends AbstractTask implements PersistableTask {
@@ -66,15 +67,27 @@ public abstract class AbstractPersistableTask extends AbstractTask implements Pe
* @param payload the persisted payload
*/
@Override
public void restore(JSONObject payload) {
public void restore(ObjectNode payload) {
// Nothing to restore by default
}
/**
* No-op default: only tasks carrying extra state need to override this.
*
* @param payload the payload being written
*/
protected void writePayload(ObjectNode payload) {
// Nothing extra to write by default
}
@Override
public String toPayload() {
JSONObject payload = new JSONObject(this);
ObjectNode payload = Json.object();
payload.put("id", getId());
payload.put("lockId", getLockId());
payload.put("name", getName());
writePayload(payload);
return payload.toString();
}
@@ -5,9 +5,10 @@ package me.bobert.netneutrality.core.tasks.impl;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.context.tasks.EmojiTaskContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import org.json.JSONObject;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -27,7 +28,7 @@ public abstract class EmojiTask extends AbstractPersistableTask {
/**
* Constructor used by the persistable task registry: the message coordinates and the emoji
* mapping are hydrated afterwards by {@link #restore(JSONObject)}.
* mapping are hydrated afterwards by {@link #restore(ObjectNode)}.
*
* @param taskManager the application task manager, must not be {@code null}
*/
@@ -36,18 +37,32 @@ public abstract class EmojiTask extends AbstractPersistableTask {
}
@Override
public void restore(JSONObject payload) {
this.guildId = payload.optString("guildId", null);
this.channelId = payload.optString("channelId", null);
this.messageId = payload.optString("messageId", null);
public void restore(ObjectNode payload) {
this.guildId = payload.path("guildId").asText(null);
this.channelId = payload.path("channelId").asText(null);
this.messageId = payload.path("messageId").asText(null);
emojiMapping.clear();
JSONObject mapping = payload.optJSONObject("emojiMapping");
if (mapping != null) {
for (String emoji : mapping.keySet()) {
emojiMapping.put(emoji, mapping.getString(emoji));
JsonNode mapping = payload.path("emojiMapping");
if (mapping.isObject()) {
mapping.properties().forEach(entry -> emojiMapping.put(entry.getKey(), entry.getValue().asText()));
}
}
@Override
protected void writePayload(ObjectNode payload) {
putIfPresent(payload, "guildId", guildId);
putIfPresent(payload, "channelId", channelId);
putIfPresent(payload, "messageId", messageId);
ObjectNode mapping = payload.putObject("emojiMapping");
emojiMapping.forEach(mapping::put);
}
private static void putIfPresent(ObjectNode payload, String key, String value) {
if (value != null) {
payload.put(key, value);
}
}
public String getGuildId() {
@@ -5,12 +5,12 @@ package me.bobert.netneutrality.core.tasks.persistence;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.bootstrap.ApplicationContext;
import me.bobert.netneutrality.bootstrap.ReflectionHelper;
import me.bobert.netneutrality.core.tasks.PersistableTask;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
import java.util.Collections;
import java.util.Map;
@@ -88,7 +88,7 @@ public final class PersistableTaskRegistry {
* @throws IllegalArgumentException if no task type is registered under {@code typeId}
* @throws ReflectiveOperationException if the task type cannot be instantiated
*/
public PersistableTask create(String typeId, JSONObject payload) throws ReflectiveOperationException {
public PersistableTask create(String typeId, ObjectNode payload) throws ReflectiveOperationException {
Class<? extends PersistableTask> type = typesById.get(typeId);
if (type == null) {
throw new IllegalArgumentException("No persistable task type registered for id: " + typeId);
@@ -5,14 +5,15 @@ package me.bobert.netneutrality.db.service;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.core.tasks.PersistableTask;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.core.tasks.persistence.PersistableTaskRegistry;
import me.bobert.netneutrality.db.entities.OTask;
import me.bobert.netneutrality.db.repository.TaskRepository;
import me.bobert.netneutrality.utils.Json;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
import java.util.List;
import java.util.Objects;
@@ -42,7 +43,7 @@ public class TaskService {
String payload = oTask.getPayload();
try {
JSONObject json = new JSONObject(payload);
ObjectNode json = Json.readObject(payload);
PersistableTask task = registry.create(typeId, json);
validateTask(task, oTask);
@@ -61,13 +62,13 @@ public class TaskService {
throw new IllegalArgumentException("Task ID mismatch, expected: " + task.getId());
}
JSONObject json = new JSONObject(payload);
String payloadId = json.getString("id");
ObjectNode json = Json.readObject(payload);
String payloadId = json.path("id").asText(null);
if (!Objects.equals(payloadId, task.getId())) {
throw new IllegalArgumentException("Task payload ID mismatch, expected: " + task.getId());
}
String payloadLockId = json.getString("lockId");
String payloadLockId = json.path("lockId").asText(null);
if (!Objects.equals(payloadLockId, task.getLockId())) {
throw new IllegalArgumentException("Task lock ID mismatch, expected: " + task.getLockId());
}
@@ -6,17 +6,17 @@ package me.bobert.netneutrality.http;
* https://discord.gg/qUa6zNFZsd
*/
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import me.bobert.netneutrality.http.ResponseCache.CachedResponse;
import me.bobert.netneutrality.utils.Json;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -130,15 +130,15 @@ public class WebModule {
return keyBuilder.toString();
}
public JSONObject getAsObject() {
public ObjectNode getAsObject() {
return cachedResponse.getBody()
.map(JSONObject::new)
.map(Json::readObject)
.orElseThrow(() -> new IllegalStateException("No body available for this response"));
}
public JSONArray getAsArray() {
public ArrayNode getAsArray() {
return cachedResponse.getBody()
.map(JSONArray::new)
.map(Json::readArray)
.orElseThrow(() -> new IllegalStateException("No body available for this response"));
}
@@ -253,7 +253,7 @@ public class WebModule {
}
if (method == Method.POST && !params.isEmpty()) {
RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8"));
RequestBody body = RequestBody.create(Json.write(params), MediaType.parse("application/json; charset=utf-8"));
builder.post(body);
}
@@ -1,33 +0,0 @@
package me.bobert.netneutrality.http.server;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import com.google.gson.Gson;
import io.javalin.json.JsonMapper;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Type;
/**
* Javalin {@link JsonMapper} backed by a shared {@link Gson} instance.
*/
public class GsonJsonMapper implements JsonMapper {
private static final Gson GSON = new Gson();
@NotNull
@Override
public String toJsonString(@NotNull Object obj, @NotNull Type type) {
return GSON.toJson(obj, type);
}
@NotNull
@Override
public <T> T fromJsonString(@NotNull String json, @NotNull Type targetType) {
return GSON.fromJson(json, targetType);
}
}
@@ -57,7 +57,6 @@ public class WebApi implements Lifecycle {
// Javalin 7 requires routes, exception and error handlers to be registered upfront.
app = Javalin.create(config -> {
config.jsonMapper(new GsonJsonMapper());
config.startup.showJavalinBanner = false;
config.routes.get("/", ctx -> ctx.html(LIVENESS_HTML));
@@ -0,0 +1,99 @@
package me.bobert.netneutrality.utils;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Process-wide JSON entry point backed by a single shared {@link ObjectMapper}.
*/
public final class Json {
private static final ObjectMapper MAPPER = new ObjectMapper();
private Json() {
}
public static ObjectMapper mapper() {
return MAPPER;
}
public static ObjectNode object() {
return MAPPER.createObjectNode();
}
public static ArrayNode array() {
return MAPPER.createArrayNode();
}
/**
* Parses a JSON document expected to be an object.
*
* @param json the raw JSON document
* @return the parsed object node
* @throws JsonException if the document is malformed or is not an object
*/
public static ObjectNode readObject(String json) {
JsonNode node = read(json);
if (!(node instanceof ObjectNode objectNode)) {
throw new JsonException("Expected a JSON object but got: " + node.getNodeType());
}
return objectNode;
}
/**
* Parses a JSON document expected to be an array.
*
* @param json the raw JSON document
* @return the parsed array node
* @throws JsonException if the document is malformed or is not an array
*/
public static ArrayNode readArray(String json) {
JsonNode node = read(json);
if (!(node instanceof ArrayNode arrayNode)) {
throw new JsonException("Expected a JSON array but got: " + node.getNodeType());
}
return arrayNode;
}
public static JsonNode read(String json) {
try {
return MAPPER.readTree(json);
} catch (JsonProcessingException e) {
throw new JsonException("Cannot parse JSON document", e);
}
}
public static String write(Object value) {
try {
return MAPPER.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new JsonException("Cannot serialize value to JSON", e);
}
}
/**
* Unchecked wrapper around Jackson's checked processing exceptions.
*/
public static class JsonException extends RuntimeException {
public JsonException(String message) {
super(message);
}
public JsonException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -6,9 +6,9 @@ package me.bobert.netneutrality.utils;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ArrayNode;
import me.bobert.netneutrality.http.WebModule;
import org.apache.logging.log4j.LogManager;
import org.json.JSONArray;
import java.io.File;
import java.io.FileInputStream;
@@ -101,11 +101,11 @@ public class Settings implements SettingsProvider {
String gitlabProjectId = "22";
String latestReleaseUrl = String.format("%s/api/v4/projects/%s/releases/", gitlabHost, gitlabProjectId);
JSONArray json = new WebModule.Builder(latestReleaseUrl).cache(1, TimeUnit.DAYS).build().request().getAsArray();
if (json.isNull(0)) {
ArrayNode json = new WebModule.Builder(latestReleaseUrl).cache(1, TimeUnit.DAYS).build().request().getAsArray();
if (json.isEmpty() || !json.get(0).hasNonNull("tag_name")) {
return "0.0.0";
}
return json.getJSONObject(0).getString("tag_name");
return json.get(0).get("tag_name").asText();
}
}
@@ -214,7 +214,7 @@ class TaskManagerImplTest {
}
@Override
public void restore(org.json.JSONObject payload) {
public void restore(com.fasterxml.jackson.databind.node.ObjectNode payload) {
}
@Override
@@ -12,7 +12,7 @@ import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.core.tasks.impl.AbstractPersistableTask;
import me.bobert.netneutrality.core.tasks.impl.EmojiTask;
import me.bobert.netneutrality.tasks.reaction.EmojiReactionRoleTask;
import org.json.JSONObject;
import me.bobert.netneutrality.utils.Json;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -49,7 +49,7 @@ class PersistableTaskRegistryTest {
registry.scan();
for (String typeId : registry.getTypeIds()) {
Class<?> type = registry.create(typeId, new JSONObject()).getClass();
Class<?> type = registry.create(typeId, Json.object()).getClass();
assertFalse(Modifier.isAbstract(type.getModifiers()), type.getName() + " must not be abstract");
assertNotEquals(AbstractPersistableTask.class, type);
@@ -61,7 +61,7 @@ class PersistableTaskRegistryTest {
void createInstantiatesRegisteredTypeThroughDi() throws Exception {
registry.scan();
PersistableTask task = registry.create("emoji-reaction-role", new JSONObject());
PersistableTask task = registry.create("emoji-reaction-role", Json.object());
assertInstanceOf(EmojiReactionRoleTask.class, task);
}
@@ -70,7 +70,7 @@ class PersistableTaskRegistryTest {
void createRejectsUnknownTypeId() {
registry.scan();
assertThrows(IllegalArgumentException.class, () -> registry.create("does-not-exist", new JSONObject()));
assertThrows(IllegalArgumentException.class, () -> registry.create("does-not-exist", Json.object()));
}
@Test
@@ -45,9 +45,9 @@ class WebModuleTest {
String url = url("cache-hit");
assertEquals(1, new WebModule.Builder(url).get().cache(1, TimeUnit.MINUTES).build().request()
.getAsObject().getInt("hits"));
.getAsObject().get("hits").asInt());
assertEquals(1, new WebModule.Builder(url).get().cache(1, TimeUnit.MINUTES).build().request()
.getAsObject().getInt("hits"));
.getAsObject().get("hits").asInt());
assertEquals(1, hits.get());
}
@@ -5,12 +5,13 @@ package me.bobert.netneutrality.tasks.reaction;
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.node.ObjectNode;
import me.bobert.netneutrality.bootstrap.ApplicationContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.core.tasks.impl.EmojiTask;
import me.bobert.netneutrality.core.tasks.persistence.PersistableTaskRegistry;
import me.bobert.netneutrality.db.service.TaskService;
import org.json.JSONObject;
import me.bobert.netneutrality.utils.Json;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -45,7 +46,7 @@ class EmojiReactionRoleTaskPersistenceTest {
Map<String, String> mapping = Map.of("\uD83D\uDD25", "role-1", "\u2705", "role-2");
EmojiReactionRoleTask original = new EmojiReactionRoleTask(taskManager, "guild-1", "channel-1", "message-1", mapping);
JSONObject payload = new JSONObject(original.toPayload());
ObjectNode payload = Json.readObject(original.toPayload());
EmojiTask restored = assertInstanceOf(EmojiTask.class, registry.create(original.typeId(), payload));
assertEquals(original.getId(), restored.getId());
@@ -68,17 +69,17 @@ class EmojiReactionRoleTaskPersistenceTest {
void payloadAlwaysCarriesIdAndLockId() {
EmojiReactionRoleTask task = new EmojiReactionRoleTask(taskManager, "g", "c", "m");
JSONObject payload = new JSONObject(task.toPayload());
ObjectNode payload = Json.readObject(task.toPayload());
assertEquals(task.getId(), payload.getString("id"));
assertEquals(task.getLockId(), payload.getString("lockId"));
assertEquals(task.getId(), payload.get("id").asText());
assertEquals(task.getLockId(), payload.get("lockId").asText());
}
@Test
void missingEmojiMappingRestoresToEmptyMap() throws Exception {
EmojiReactionRoleTask original = new EmojiReactionRoleTask(taskManager, "guild-1", "channel-1", "message-1");
JSONObject payload = new JSONObject(original.toPayload());
ObjectNode payload = Json.readObject(original.toPayload());
payload.remove("emojiMapping");
EmojiTask restored = assertInstanceOf(EmojiTask.class, registry.create(original.typeId(), payload));
@@ -90,7 +91,7 @@ class EmojiReactionRoleTaskPersistenceTest {
void restoredTaskHasInjectedTaskManager() throws Exception {
EmojiReactionRoleTask original = new EmojiReactionRoleTask(taskManager, "guild-1", "channel-1", "message-1");
JSONObject payload = new JSONObject(original.toPayload());
ObjectNode payload = Json.readObject(original.toPayload());
EmojiTask restored = assertInstanceOf(EmojiTask.class, registry.create(original.typeId(), payload));
restored.addEmoji("\uD83D\uDD25", "role-1");
@@ -98,4 +99,19 @@ class EmojiReactionRoleTaskPersistenceTest {
assertEquals("role-1", restored.getEmojiMapping().get("\uD83D\uDD25"));
verify(taskService).persistTask(restored);
}
@Test
void legacyPayloadStillRestores() throws Exception {
String legacy = "{\"id\":\"emoji-reaction-role-guild-1-message-1\",\"lockId\":\"lock-1\",\"name\":\"emoji-reaction-role\","
+ "\"guildId\":\"guild-1\",\"channelId\":\"channel-1\",\"messageId\":\"message-1\","
+ "\"emojiMapping\":{\"\uD83D\uDD25\":\"role-1\"}}";
EmojiTask restored = assertInstanceOf(EmojiTask.class,
registry.create("emoji-reaction-role", Json.readObject(legacy)));
assertEquals("guild-1", restored.getGuildId());
assertEquals("channel-1", restored.getChannelId());
assertEquals("message-1", restored.getMessageId());
assertEquals(Map.of("\uD83D\uDD25", "role-1"), restored.getEmojiMapping());
}
}