Skip to content

Commit

Permalink
Fix slackapi#1426 IllegalStateException when deserializing message us…
Browse files Browse the repository at this point in the history
…ing conversation.history
  • Loading branch information
seratch committed Jan 28, 2025
1 parent 0764bd8 commit c79b3ea
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.slack.api.SlackConfig;
import com.slack.api.audit.response.LogsResponse;
import com.slack.api.model.Attachment;
import com.slack.api.model.File;
import com.slack.api.model.admin.AppWorkflow;
import com.slack.api.model.block.ContextBlockElement;
import com.slack.api.model.block.LayoutBlock;
Expand All @@ -31,6 +32,7 @@ public static Gson createSnakeCase() {
return new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant.class, new JavaTimeInstantFactory())
.registerTypeAdapter(File.class, new GsonFileFactory())
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory())
.registerTypeAdapter(TextObject.class, new GsonTextObjectFactory())
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory())
Expand All @@ -54,6 +56,7 @@ public static Gson createSnakeCase(SlackConfig config) {
GsonBuilder gsonBuilder = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps))
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps))
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProps))
.registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProps))
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps))
Expand Down Expand Up @@ -82,6 +85,7 @@ public static Gson createCamelCase(SlackConfig config) {
boolean failOnUnknownProps = config.isFailOnUnknownProperties();
GsonBuilder gsonBuilder = new GsonBuilder()
.registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps))
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps))
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProps))
.registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProps))
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.slack.api.util.json;

import com.google.gson.*;
import com.slack.api.model.File;

import java.lang.reflect.Type;

public class GsonFileFactory implements JsonDeserializer<File>, JsonSerializer<File> {

// This is just a workaround to customize Gson library behavior
// You don't need to edit this class at all
static class NormalizedFile extends File {
}

private boolean failOnUnknownProperties;

public GsonFileFactory() {
this(false);
}

public GsonFileFactory(boolean failOnUnknownProperties) {
this.failOnUnknownProperties = failOnUnknownProperties;
}

@Override
public File deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
// Remove unusual data structure form Slack API server
// See https://github.com/slackapi/java-slack-sdk/issues/1426 for more details
if (jsonObject.has("groups") && !jsonObject.get("groups").isJsonArray()) {
// As the starting point in Jan 2025, we just ignore this property,
// but we may want to assign to a different field if it's necessary for some use cases
jsonObject.remove("groups");
}
if (jsonObject.has("shares")) {
JsonObject shares = jsonObject.get("shares").getAsJsonObject();
if (shares.has("public")) {
adjustSharesObjects(shares.get("public").getAsJsonObject());
}
if (shares.has("private")) {
adjustSharesObjects(shares.get("private").getAsJsonObject());
}
}
// To prevent StackOverflowError here, run the deserialize method for File's subclass.
// If we want to attach the above unusual data, you can add it to File class
return context.deserialize(jsonObject, NormalizedFile.class);
}

private void adjustSharesObjects(JsonObject shares) {
for (String channelId : shares.keySet()) {
for (JsonElement elem : shares.get(channelId).getAsJsonArray()) {
JsonObject e = elem.getAsJsonObject();
if (e.has("reply_users") && !e.get("reply_users").isJsonArray()) {
// As the starting point in Jan 2025, we just ignore this property,
// but we may want to assign to a different field if it's necessary for some use cases
e.remove("reply_users");
}
}
}
}

@Override
public JsonElement serialize(File src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src);
}
}
91 changes: 91 additions & 0 deletions slack-api-model/src/test/java/test_locally/api/model/FileTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package test_locally.api.model;

import com.slack.api.model.File;
import org.junit.Test;
import test_locally.unit.GsonFactory;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;

public class FileTest {

String ISSUE_1426_JSON = "{\n" +
" \"id\": \"F08AF7HUWQL\",\n" +
" \"created\": 1737949586,\n" +
" \"timestamp\": 1737949586,\n" +
" \"name\": \"sample.txt\",\n" +
" \"title\": \"sample.txt\",\n" +
" \"mimetype\": \"text/plain\",\n" +
" \"filetype\": \"text\",\n" +
" \"pretty_type\": \"Plain Text\",\n" +
" \"user\": \"U8P5K48E6\",\n" +
" \"user_team\": \"T03E94MJU\",\n" +
" \"editable\": true,\n" +
" \"size\": 57,\n" +
" \"mode\": \"snippet\",\n" +
" \"is_external\": false,\n" +
" \"external_type\": \"\",\n" +
" \"is_public\": true,\n" +
" \"public_url_shared\": false,\n" +
" \"display_as_bot\": false,\n" +
" \"username\": \"\",\n" +
" \"url_private\": \"https://files.slack.com/files-pri/T03E94MJU-F08AF7HUWQL/sample.txt\",\n" +
" \"url_private_download\": \"https://files.slack.com/files-pri/T03E94MJU-F08AF7HUWQL/download/sample.txt\",\n" +
" \"permalink\": \"https://seratch.slack.com/files/U8P5K48E6/F08AF7HUWQL/sample.txt\",\n" +
" \"permalink_public\": \"https://slack-files.com/T03E94MJU-F08AF7HUWQL-9d27bd3319\",\n" +
" \"edit_link\": \"https://seratch.slack.com/files/U8P5K48E6/F08AF7HUWQL/sample.txt/edit\",\n" +
" \"preview\": \"Hello, World!!!!!\\n\\nThis is a sample text file.\\n\\n日本語\",\n" +
" \"preview_highlight\": \"\\u003cdiv class\\u003d\\\"CodeMirror cm-s-default CodeMirrorServer\\\"\\u003e\\n\\u003cdiv class\\u003d\\\"CodeMirror-code\\\"\\u003e\\n\\u003cdiv\\u003e\\u003cpre\\u003eHello, World!!!!!\\u003c/pre\\u003e\\u003c/div\\u003e\\n\\u003cdiv\\u003e\\u003cpre\\u003e\\u003c/pre\\u003e\\u003c/div\\u003e\\n\\u003cdiv\\u003e\\u003cpre\\u003eThis is a sample text file.\\u003c/pre\\u003e\\u003c/div\\u003e\\n\\u003cdiv\\u003e\\u003cpre\\u003e\\u003c/pre\\u003e\\u003c/div\\u003e\\n\\u003cdiv\\u003e\\u003cpre\\u003e日本語\\u003c/pre\\u003e\\u003c/div\\u003e\\n\\u003c/div\\u003e\\n\\u003c/div\\u003e\\n\",\n" +
" \"lines\": 5,\n" +
" \"lines_more\": 0,\n" +
" \"preview_is_truncated\": false,\n" +
" \"favorites\": [],\n" +
" \"is_starred\": false,\n" +
" \"shares\": {\n" +
" \"public\": {\n" +
" \"C03E94MKU\": [\n" +
" {\n" +
" \"reply_users\": {},\n" + // unusual data structure here
" \"reply_users_count\": 0,\n" +
" \"reply_count\": 0,\n" +
" \"ts\": \"1737949587.842889\",\n" +
" \"channel_name\": \"random\",\n" +
" \"team_id\": \"T03E94MJU\",\n" +
" \"share_user_id\": \"U8P5K48E6\",\n" +
" \"source\": \"UNKNOWN\"\n" +
" }\n" +
" ],\n" +
" \"C03E94MKS\": [\n" +
" {\n" +
" \"reply_users\": [],\n" +
" \"reply_users_count\": 0,\n" +
" \"reply_count\": 0,\n" +
" \"ts\": \"1737949587.678069\",\n" +
" \"channel_name\": \"general\",\n" +
" \"team_id\": \"T03E94MJU\",\n" +
" \"share_user_id\": \"U8P5K48E6\",\n" +
" \"source\": \"UNKNOWN\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"channels\": [\n" +
" \"C03E94MKU\",\n" +
" \"C03E94MKS\"\n" +
" ],\n" +
" \"groups\": {},\n" + // unusual data structure here
" \"ims\": [],\n" +
" \"has_more_shares\": false,\n" +
" \"has_rich_preview\": false,\n" +
" \"file_access\": \"visible\",\n" +
" \"comments_count\": 0\n" +
"}\n";

@Test
public void issue1426_parse() {
// com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 65 column 14 path $.groups
File message = GsonFactory.createSnakeCase().fromJson(ISSUE_1426_JSON, File.class);
assertThat(message.getShares(), is(notNullValue()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.slack.api.model.Attachment;
import com.slack.api.model.File;
import com.slack.api.model.block.ContextBlockElement;
import com.slack.api.model.block.LayoutBlock;
import com.slack.api.model.block.composition.TextObject;
Expand All @@ -28,6 +29,7 @@ public static Gson createSnakeCaseWithoutUnknownPropertyDetection(boolean failOn
public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unknownPropertyDetection) {
GsonBuilder builder = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProperties))
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProperties))
.registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProperties))
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProperties))
Expand Down

0 comments on commit c79b3ea

Please sign in to comment.