Skip to content

Commit fc999b9

Browse files
Copilotdargmuesli
andauthored
Fix numeric strings being parsed as integers in JSON request bodies (#421)
* Initial plan * Fix numeric string parsing issue in JSON request bodies Co-authored-by: dargmuesli <4778485+dargmuesli@users.noreply.github.com> * Preserve numeric types for known numeric parameters while keeping string parameters as strings Co-authored-by: dargmuesli <4778485+dargmuesli@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dargmuesli <4778485+dargmuesli@users.noreply.github.com>
1 parent 7af222e commit fc999b9

File tree

3 files changed

+183
-7
lines changed

3 files changed

+183
-7
lines changed

src/main/java/se/michaelthelin/spotify/requests/AbstractRequest.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,33 @@ public void initializeBody() {
8888

8989
public String bodyParametersToJson(List<NameValuePair> bodyParameters) {
9090
JsonObject jsonObject = new JsonObject();
91-
JsonElement jsonElement;
91+
92+
// Parameters that should be parsed as their original types (not strings)
93+
// These are parameters that accept Integer, Boolean, JsonArray, or JsonObject types
94+
java.util.Set<String> numericAndStructuredParams = java.util.Set.of(
95+
"collaborative", "device_ids", "ids", "insert_before", "offset",
96+
"play", "position", "position_ms", "public", "range_length",
97+
"range_start", "tracks", "uris"
98+
);
9299

93100
for (NameValuePair nameValuePair : bodyParameters) {
94-
try {
95-
jsonElement = JsonParser.parseString(nameValuePair.getValue());
96-
} catch (JsonSyntaxException e) {
97-
jsonElement = new JsonPrimitive(nameValuePair.getValue());
101+
String name = nameValuePair.getName();
102+
String value = nameValuePair.getValue();
103+
104+
if (numericAndStructuredParams.contains(name)) {
105+
// For known numeric/boolean/structured parameters, parse as JSON to preserve type
106+
try {
107+
JsonElement jsonElement = JsonParser.parseString(value);
108+
jsonObject.add(name, jsonElement);
109+
} catch (JsonSyntaxException e) {
110+
// Fallback to string if parsing fails
111+
jsonObject.addProperty(name, value);
112+
}
113+
} else {
114+
// For string parameters (like name, description), always keep as string
115+
// This prevents numeric strings like "2025" from being converted to numbers
116+
jsonObject.addProperty(name, value);
98117
}
99-
100-
jsonObject.add(nameValuePair.getName(), jsonElement);
101118
}
102119

103120
return jsonObject.toString();
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package se.michaelthelin.spotify.requests.data.player;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import org.junit.jupiter.api.Test;
6+
import se.michaelthelin.spotify.SpotifyApi;
7+
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
10+
/**
11+
* Test for ensuring numeric and structured parameters are preserved with correct types in JSON request bodies
12+
*/
13+
public class StartResumeUsersPlaybackRequestNumericTest {
14+
15+
@Test
16+
public void shouldPreserveNumericParametersAsNumbers() {
17+
// Create a request with numeric position_ms parameter
18+
StartResumeUsersPlaybackRequest request = new SpotifyApi.Builder()
19+
.setAccessToken("test-token")
20+
.build()
21+
.startResumeUsersPlayback()
22+
.position_ms(10000)
23+
.build();
24+
25+
// Generate the JSON body
26+
String json = request.bodyParametersToJson(request.getBodyParameters());
27+
28+
// The JSON should contain unquoted number, not a string
29+
assertTrue(json.contains("\"position_ms\":10000"),
30+
"position_ms should be an unquoted number. Actual JSON: " + json);
31+
32+
// Should NOT contain quoted string
33+
assertTrue(!json.contains("\"position_ms\":\"10000\""),
34+
"position_ms should not be a quoted string. Actual JSON: " + json);
35+
}
36+
37+
@Test
38+
public void shouldPreserveBooleanParametersAsBooleans() {
39+
// Create a request with JSON array uris
40+
JsonArray uris = new JsonArray();
41+
uris.add("spotify:track:test1");
42+
uris.add("spotify:track:test2");
43+
44+
StartResumeUsersPlaybackRequest request = new SpotifyApi.Builder()
45+
.setAccessToken("test-token")
46+
.build()
47+
.startResumeUsersPlayback()
48+
.uris(uris)
49+
.build();
50+
51+
// Generate the JSON body
52+
String json = request.bodyParametersToJson(request.getBodyParameters());
53+
54+
// The JSON should contain a proper JSON array
55+
assertTrue(json.contains("\"uris\":["),
56+
"uris should be a JSON array. Actual JSON: " + json);
57+
assertTrue(json.contains("spotify:track:test1"),
58+
"uris should contain the track URIs. Actual JSON: " + json);
59+
}
60+
61+
@Test
62+
public void shouldPreserveJsonObjectParameters() {
63+
// Create a request with JSON object offset
64+
JsonObject offset = new JsonObject();
65+
offset.addProperty("position", 5);
66+
67+
StartResumeUsersPlaybackRequest request = new SpotifyApi.Builder()
68+
.setAccessToken("test-token")
69+
.build()
70+
.startResumeUsersPlayback()
71+
.offset(offset)
72+
.build();
73+
74+
// Generate the JSON body
75+
String json = request.bodyParametersToJson(request.getBodyParameters());
76+
77+
// The JSON should contain a proper JSON object
78+
assertTrue(json.contains("\"offset\":{"),
79+
"offset should be a JSON object. Actual JSON: " + json);
80+
assertTrue(json.contains("\"position\":5"),
81+
"offset should contain the position property. Actual JSON: " + json);
82+
}
83+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package se.michaelthelin.spotify.requests.data.playlists;
2+
3+
import org.junit.jupiter.api.Test;
4+
import se.michaelthelin.spotify.SpotifyApi;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
/**
10+
* Test for ensuring numeric strings are preserved as strings in JSON request bodies
11+
*/
12+
public class CreatePlaylistRequestNumericStringTest {
13+
14+
@Test
15+
public void shouldPreserveNumericStringsAsStrings() {
16+
// Create a request with numeric strings for name and description
17+
CreatePlaylistRequest request = new SpotifyApi.Builder()
18+
.setAccessToken("test-token")
19+
.build()
20+
.createPlaylist("testuser", "2025")
21+
.description("2025")
22+
.build();
23+
24+
// Generate the JSON body
25+
String json = request.bodyParametersToJson(request.getBodyParameters());
26+
27+
// The JSON should contain quoted strings, not numbers
28+
assertTrue(json.contains("\"name\":\"2025\""),
29+
"Name should be a quoted string, not a number. Actual JSON: " + json);
30+
assertTrue(json.contains("\"description\":\"2025\""),
31+
"Description should be a quoted string, not a number. Actual JSON: " + json);
32+
33+
// Should NOT contain unquoted numbers
34+
assertTrue(!json.contains("\"name\":2025"),
35+
"Name should not be an unquoted number. Actual JSON: " + json);
36+
assertTrue(!json.contains("\"description\":2025"),
37+
"Description should not be an unquoted number. Actual JSON: " + json);
38+
}
39+
40+
@Test
41+
public void shouldPreserveMixedTypesCorrectly() {
42+
// Test various edge cases
43+
CreatePlaylistRequest request = new SpotifyApi.Builder()
44+
.setAccessToken("test-token")
45+
.build()
46+
.createPlaylist("testuser", "123.45") // decimal number as string
47+
.description("abc123") // mixed alphanumeric
48+
.build();
49+
50+
String json = request.bodyParametersToJson(request.getBodyParameters());
51+
52+
// Both should remain as strings
53+
assertTrue(json.contains("\"name\":\"123.45\""),
54+
"Decimal number string should remain quoted. Actual JSON: " + json);
55+
assertTrue(json.contains("\"description\":\"abc123\""),
56+
"Mixed alphanumeric should remain quoted. Actual JSON: " + json);
57+
}
58+
59+
@Test
60+
public void shouldPreserveEmptyAndNullLikeStrings() {
61+
CreatePlaylistRequest request = new SpotifyApi.Builder()
62+
.setAccessToken("test-token")
63+
.build()
64+
.createPlaylist("testuser", "null")
65+
.description("true")
66+
.build();
67+
68+
String json = request.bodyParametersToJson(request.getBodyParameters());
69+
70+
// Should remain as quoted strings, not become null/boolean literals
71+
assertTrue(json.contains("\"name\":\"null\""),
72+
"String 'null' should remain quoted. Actual JSON: " + json);
73+
assertTrue(json.contains("\"description\":\"true\""),
74+
"String 'true' should remain quoted. Actual JSON: " + json);
75+
}
76+
}

0 commit comments

Comments
 (0)