Skip to content

Commit 5593174

Browse files
authored
Fix #1426 IllegalStateException when deserializing message using conversations.history (#1429)
* Fix #1426 IllegalStateException when deserializing message using conversation.history * Handle more properties
1 parent 0764bd8 commit 5593174

File tree

4 files changed

+170
-0
lines changed

4 files changed

+170
-0
lines changed

slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.slack.api.SlackConfig;
77
import com.slack.api.audit.response.LogsResponse;
88
import com.slack.api.model.Attachment;
9+
import com.slack.api.model.File;
910
import com.slack.api.model.admin.AppWorkflow;
1011
import com.slack.api.model.block.ContextBlockElement;
1112
import com.slack.api.model.block.LayoutBlock;
@@ -31,6 +32,7 @@ public static Gson createSnakeCase() {
3132
return new GsonBuilder()
3233
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
3334
.registerTypeAdapter(Instant.class, new JavaTimeInstantFactory())
35+
.registerTypeAdapter(File.class, new GsonFileFactory())
3436
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory())
3537
.registerTypeAdapter(TextObject.class, new GsonTextObjectFactory())
3638
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory())
@@ -54,6 +56,7 @@ public static Gson createSnakeCase(SlackConfig config) {
5456
GsonBuilder gsonBuilder = new GsonBuilder()
5557
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
5658
.registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps))
59+
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps))
5760
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProps))
5861
.registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProps))
5962
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps))
@@ -82,6 +85,7 @@ public static Gson createCamelCase(SlackConfig config) {
8285
boolean failOnUnknownProps = config.isFailOnUnknownProperties();
8386
GsonBuilder gsonBuilder = new GsonBuilder()
8487
.registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps))
88+
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps))
8589
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProps))
8690
.registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProps))
8791
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps))
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.slack.api.util.json;
2+
3+
import com.google.gson.*;
4+
import com.slack.api.model.File;
5+
6+
import java.lang.reflect.Type;
7+
8+
public class GsonFileFactory implements JsonDeserializer<File>, JsonSerializer<File> {
9+
10+
// This is just a workaround to customize Gson library behavior
11+
// You don't need to edit this class at all
12+
static class NormalizedFile extends File {
13+
}
14+
15+
private boolean failOnUnknownProperties;
16+
17+
public GsonFileFactory() {
18+
this(false);
19+
}
20+
21+
public GsonFileFactory(boolean failOnUnknownProperties) {
22+
this.failOnUnknownProperties = failOnUnknownProperties;
23+
}
24+
25+
@Override
26+
public File deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
27+
throws JsonParseException {
28+
final JsonObject jsonObject = json.getAsJsonObject();
29+
// Remove unusual data structure form Slack API server
30+
// As the starting point in Jan 2025, we just ignore these non-array properties,
31+
// but we may want to assign to a different field if it's necessary for some use cases
32+
// See https://github.com/slackapi/java-slack-sdk/issues/1426 for more details
33+
if (jsonObject.has("favorites") && !jsonObject.get("favorites").isJsonArray()) {
34+
jsonObject.remove("favorites");
35+
}
36+
if (jsonObject.has("channels") && !jsonObject.get("channels").isJsonArray()) {
37+
jsonObject.remove("channels");
38+
}
39+
if (jsonObject.has("ims") && !jsonObject.get("ims").isJsonArray()) {
40+
jsonObject.remove("ims");
41+
}
42+
if (jsonObject.has("groups") && !jsonObject.get("groups").isJsonArray()) {
43+
jsonObject.remove("groups");
44+
}
45+
if (jsonObject.has("shares")) {
46+
JsonObject shares = jsonObject.get("shares").getAsJsonObject();
47+
if (shares.has("public")) {
48+
adjustSharesObjects(shares.get("public").getAsJsonObject());
49+
}
50+
if (shares.has("private")) {
51+
adjustSharesObjects(shares.get("private").getAsJsonObject());
52+
}
53+
}
54+
// To prevent StackOverflowError here, run the deserialize method for File's subclass.
55+
// If we want to attach the above unusual data, you can add it to File class
56+
return context.deserialize(jsonObject, NormalizedFile.class);
57+
}
58+
59+
private void adjustSharesObjects(JsonObject shares) {
60+
for (String channelId : shares.keySet()) {
61+
for (JsonElement elem : shares.get(channelId).getAsJsonArray()) {
62+
JsonObject e = elem.getAsJsonObject();
63+
if (e.has("reply_users") && !e.get("reply_users").isJsonArray()) {
64+
// As the starting point in Jan 2025, we just ignore this property,
65+
// but we may want to assign to a different field if it's necessary for some use cases
66+
e.remove("reply_users");
67+
}
68+
}
69+
}
70+
}
71+
72+
@Override
73+
public JsonElement serialize(File src, Type typeOfSrc, JsonSerializationContext context) {
74+
return context.serialize(src);
75+
}
76+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package test_locally.api.model;
2+
3+
import com.slack.api.model.File;
4+
import org.junit.Test;
5+
import test_locally.unit.GsonFactory;
6+
7+
import static org.hamcrest.CoreMatchers.is;
8+
import static org.hamcrest.CoreMatchers.notNullValue;
9+
import static org.hamcrest.MatcherAssert.assertThat;
10+
11+
public class FileTest {
12+
13+
String ISSUE_1426_JSON = "{\n" +
14+
" \"id\": \"F08AF7HUWQL\",\n" +
15+
" \"created\": 1737949586,\n" +
16+
" \"timestamp\": 1737949586,\n" +
17+
" \"name\": \"sample.txt\",\n" +
18+
" \"title\": \"sample.txt\",\n" +
19+
" \"mimetype\": \"text/plain\",\n" +
20+
" \"filetype\": \"text\",\n" +
21+
" \"pretty_type\": \"Plain Text\",\n" +
22+
" \"user\": \"U8P5K48E6\",\n" +
23+
" \"user_team\": \"T03E94MJU\",\n" +
24+
" \"editable\": true,\n" +
25+
" \"size\": 57,\n" +
26+
" \"mode\": \"snippet\",\n" +
27+
" \"is_external\": false,\n" +
28+
" \"external_type\": \"\",\n" +
29+
" \"is_public\": true,\n" +
30+
" \"public_url_shared\": false,\n" +
31+
" \"display_as_bot\": false,\n" +
32+
" \"username\": \"\",\n" +
33+
" \"url_private\": \"https://files.slack.com/files-pri/T03E94MJU-F08AF7HUWQL/sample.txt\",\n" +
34+
" \"url_private_download\": \"https://files.slack.com/files-pri/T03E94MJU-F08AF7HUWQL/download/sample.txt\",\n" +
35+
" \"permalink\": \"https://seratch.slack.com/files/U8P5K48E6/F08AF7HUWQL/sample.txt\",\n" +
36+
" \"permalink_public\": \"https://slack-files.com/T03E94MJU-F08AF7HUWQL-9d27bd3319\",\n" +
37+
" \"edit_link\": \"https://seratch.slack.com/files/U8P5K48E6/F08AF7HUWQL/sample.txt/edit\",\n" +
38+
" \"preview\": \"Hello, World!!!!!\\n\\nThis is a sample text file.\\n\\n日本語\",\n" +
39+
" \"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" +
40+
" \"lines\": 5,\n" +
41+
" \"lines_more\": 0,\n" +
42+
" \"preview_is_truncated\": false,\n" +
43+
" \"favorites\": {},\n" + // unusual data structure here
44+
" \"is_starred\": false,\n" +
45+
" \"shares\": {\n" +
46+
" \"public\": {\n" +
47+
" \"C03E94MKU\": [\n" +
48+
" {\n" +
49+
" \"reply_users\": {},\n" + // unusual data structure here
50+
" \"reply_users_count\": 0,\n" +
51+
" \"reply_count\": 0,\n" +
52+
" \"ts\": \"1737949587.842889\",\n" +
53+
" \"channel_name\": \"random\",\n" +
54+
" \"team_id\": \"T03E94MJU\",\n" +
55+
" \"share_user_id\": \"U8P5K48E6\",\n" +
56+
" \"source\": \"UNKNOWN\"\n" +
57+
" }\n" +
58+
" ],\n" +
59+
" \"C03E94MKS\": [\n" +
60+
" {\n" +
61+
" \"reply_users\": [],\n" +
62+
" \"reply_users_count\": 0,\n" +
63+
" \"reply_count\": 0,\n" +
64+
" \"ts\": \"1737949587.678069\",\n" +
65+
" \"channel_name\": \"general\",\n" +
66+
" \"team_id\": \"T03E94MJU\",\n" +
67+
" \"share_user_id\": \"U8P5K48E6\",\n" +
68+
" \"source\": \"UNKNOWN\"\n" +
69+
" }\n" +
70+
" ]\n" +
71+
" }\n" +
72+
" },\n" +
73+
" \"channels\": {},\n" + // unusual data structure here
74+
" \"groups\": {},\n" + // unusual data structure here
75+
" \"ims\": {},\n" + // unusual data structure here
76+
" \"has_more_shares\": false,\n" +
77+
" \"has_rich_preview\": false,\n" +
78+
" \"file_access\": \"visible\",\n" +
79+
" \"comments_count\": 0\n" +
80+
"}\n";
81+
82+
@Test
83+
public void issue1426_parse() {
84+
// com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 65 column 14 path $.groups
85+
File file = GsonFactory.createSnakeCase().fromJson(ISSUE_1426_JSON, File.class);
86+
assertThat(file.getShares(), is(notNullValue()));
87+
}
88+
}

slack-api-model/src/test/java/test_locally/unit/GsonFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.google.gson.Gson;
55
import com.google.gson.GsonBuilder;
66
import com.slack.api.model.Attachment;
7+
import com.slack.api.model.File;
78
import com.slack.api.model.block.ContextBlockElement;
89
import com.slack.api.model.block.LayoutBlock;
910
import com.slack.api.model.block.composition.TextObject;
@@ -28,6 +29,7 @@ public static Gson createSnakeCaseWithoutUnknownPropertyDetection(boolean failOn
2829
public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unknownPropertyDetection) {
2930
GsonBuilder builder = new GsonBuilder()
3031
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
32+
.registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProperties))
3133
.registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProperties))
3234
.registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProperties))
3335
.registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProperties))

0 commit comments

Comments
 (0)