Skip to content

Commit 446fffd

Browse files
Merge pull request #2176 from stripe/xavdid/merge-java-private-preview
Merge to private-preview
2 parents 4353f5f + 795b2fc commit 446fffd

11 files changed

Lines changed: 266 additions & 22 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ This release uses the API version `2026-01-28.preview`.
6363
* Add support for `url` on `financialconnections.Session`
6464
* Add support for `billingCycleAnchor` on `SubscriptionCreateParams.trial_settings.end_behavior` and `SubscriptionUpdateParams.trial_settings.end_behavior`
6565

66+
## 31.4.1 - 2026-03-06
67+
* [#2168](https://github.com/stripe/stripe-java/pull/2168) Support serializing Stripe objects with ApiResource.GSON
68+
* `ApiResource.GSON` now supports serializing Stripe objects back into compatible JSON
69+
* [#2165](https://github.com/stripe/stripe-java/pull/2165) Add AI Agent information to UserAgent
70+
6671
## 31.4.0 - 2026-02-25
6772
This release changes the pinned API version to `2026-02-25.clover`.
6873

src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
2525
}
2626
final String discriminator = "object";
2727
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
28-
final TypeAdapter<com.stripe.model.BalanceTransactionSource> balanceTransactionSourceAdapter =
29-
gson.getDelegateAdapter(
30-
this, TypeToken.get(com.stripe.model.BalanceTransactionSource.class));
3128
final TypeAdapter<com.stripe.model.ApplicationFee> applicationFeeAdapter =
3229
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ApplicationFee.class));
3330
final TypeAdapter<com.stripe.model.Charge> chargeAdapter =
@@ -68,7 +65,13 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
6865
new TypeAdapter<BalanceTransactionSource>() {
6966
@Override
7067
public void write(JsonWriter out, BalanceTransactionSource value) throws IOException {
71-
balanceTransactionSourceAdapter.write(out, value);
68+
@SuppressWarnings("unchecked")
69+
TypeAdapter<BalanceTransactionSource> adapter =
70+
(TypeAdapter<BalanceTransactionSource>)
71+
gson.getDelegateAdapter(
72+
BalanceTransactionSourceTypeAdapterFactory.this,
73+
TypeToken.get(value.getClass()));
74+
adapter.write(out, value);
7275
}
7376

7477
@Override

src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
2828
}
2929
final String discriminator = "object";
3030
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
31-
final TypeAdapter<com.stripe.model.ExternalAccount> externalAccountAdapter =
32-
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ExternalAccount.class));
3331
final TypeAdapter<com.stripe.model.BankAccount> bankAccountAdapter =
3432
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.BankAccount.class));
3533
final TypeAdapter<com.stripe.model.Card> cardAdapter =
@@ -39,7 +37,12 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
3937
new TypeAdapter<ExternalAccount>() {
4038
@Override
4139
public void write(JsonWriter out, ExternalAccount value) throws IOException {
42-
externalAccountAdapter.write(out, value);
40+
@SuppressWarnings("unchecked")
41+
TypeAdapter<ExternalAccount> adapter =
42+
(TypeAdapter<ExternalAccount>)
43+
gson.getDelegateAdapter(
44+
ExternalAccountTypeAdapterFactory.this, TypeToken.get(value.getClass()));
45+
adapter.write(out, value);
4346
}
4447

4548
@Override

src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
2525
}
2626
final String discriminator = "object";
2727
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
28-
final TypeAdapter<com.stripe.model.PaymentSource> paymentSourceAdapter =
29-
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.PaymentSource.class));
3028
final TypeAdapter<com.stripe.model.Account> accountAdapter =
3129
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.Account.class));
3230
final TypeAdapter<com.stripe.model.BankAccount> bankAccountAdapter =
@@ -40,7 +38,12 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
4038
new TypeAdapter<PaymentSource>() {
4139
@Override
4240
public void write(JsonWriter out, PaymentSource value) throws IOException {
43-
paymentSourceAdapter.write(out, value);
41+
@SuppressWarnings("unchecked")
42+
TypeAdapter<PaymentSource> adapter =
43+
(TypeAdapter<PaymentSource>)
44+
gson.getDelegateAdapter(
45+
PaymentSourceTypeAdapterFactory.this, TypeToken.get(value.getClass()));
46+
adapter.write(out, value);
4447
}
4548

4649
@Override
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.stripe.model;
2+
3+
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonNull;
5+
import com.google.gson.JsonSerializationContext;
6+
import com.google.gson.JsonSerializer;
7+
import java.lang.reflect.Type;
8+
9+
public class StripeRawJsonObjectSerializer implements JsonSerializer<StripeRawJsonObject> {
10+
@Override
11+
public JsonElement serialize(
12+
StripeRawJsonObject src, Type typeOfSrc, JsonSerializationContext context) {
13+
if (src.json != null) {
14+
return src.json;
15+
}
16+
return JsonNull.INSTANCE;
17+
}
18+
}

src/main/java/com/stripe/model/v2/core/Event.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,15 @@ protected StripeObject fetchRelatedObject(RelatedObject relatedObject) throws St
9898
objectClass = StripeRawJsonObject.class;
9999
}
100100

101-
RequestOptions opts = null;
101+
RequestOptions.RequestOptionsBuilder optsBuilder = new RequestOptions.RequestOptionsBuilder();
102+
// optsBuilder.setStripeRequestTrigger("event=" + id); // TODO https://go/j/DEVSDK-3018
102103

103104
if (context != null) {
104-
opts = new RequestOptions.RequestOptionsBuilder().setStripeAccount(context).build();
105+
optsBuilder.setStripeAccount(context);
105106
}
106107

108+
RequestOptions opts = optsBuilder.build();
109+
107110
return this.responseGetter.request(
108111
new ApiRequest(
109112
BaseAddress.API, ApiResource.RequestMethod.GET, relatedObject.getUrl(), null, opts),

src/main/java/com/stripe/net/ApiResource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ private static Gson createGson(boolean shouldSetResponseGetter) {
5757
.registerTypeAdapter(Event.Request.class, new EventRequestDeserializer())
5858
.registerTypeAdapter(StripeContext.class, new StripeContextDeserializer())
5959
.registerTypeAdapter(ExpandableField.class, new ExpandableFieldDeserializer())
60+
.registerTypeAdapter(ExpandableField.class, new ExpandableFieldSerializer())
6061
.registerTypeAdapter(Instant.class, new InstantDeserializer())
6162
.registerTypeAdapterFactory(new EventTypeAdapterFactory())
6263
.registerTypeAdapter(StripeRawJsonObject.class, new StripeRawJsonObjectDeserializer())
64+
.registerTypeAdapter(StripeRawJsonObject.class, new StripeRawJsonObjectSerializer())
6365
.registerTypeAdapterFactory(new StripeCollectionItemTypeSettingFactory())
6466
.addReflectionAccessFilter(
6567
new ReflectionAccessFilter() {

src/main/java/com/stripe/net/HttpClient.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,18 @@ static String detectAIAgent() {
144144

145145
static String detectAIAgent(Function<String, String> getEnv) {
146146
String[][] agents = {
147+
// The beginning of the section generated from our OpenAPI spec
147148
{"ANTIGRAVITY_CLI_ALIAS", "antigravity"},
148149
{"CLAUDECODE", "claude_code"},
149150
{"CLINE_ACTIVE", "cline"},
150151
{"CODEX_SANDBOX", "codex_cli"},
152+
{"CODEX_THREAD_ID", "codex_cli"},
153+
{"CODEX_SANDBOX_NETWORK_DISABLED", "codex_cli"},
154+
{"CODEX_CI", "codex_cli"},
151155
{"CURSOR_AGENT", "cursor"},
152156
{"GEMINI_CLI", "gemini_cli"},
153157
{"OPENCODE", "open_code"},
158+
// The end of the section generated from our OpenAPI spec
154159
};
155160
for (String[] agent : agents) {
156161
String val = getEnv.apply(agent[0]);
@@ -196,23 +201,23 @@ protected static String buildXStripeClientUserAgentString() {
196201
}
197202

198203
static String buildXStripeClientUserAgentString(String aiAgent) {
199-
String[] propertyNames = {
200-
"os.name",
201-
"os.version",
202-
"os.arch",
203-
"java.version",
204-
"java.vendor",
205-
"java.vm.version",
206-
"java.vm.vendor"
207-
};
204+
String[] propertyNames = {"java.version", "java.vendor", "java.vm.version", "java.vm.vendor"};
208205

209206
Map<String, String> propertyMap = new HashMap<>();
210207
for (String propertyName : propertyNames) {
211208
propertyMap.put(propertyName, System.getProperty(propertyName));
212209
}
213210
propertyMap.put("bindings.version", Stripe.VERSION);
214211
propertyMap.put("lang", "Java");
215-
propertyMap.put("publisher", "Stripe");
212+
if (Stripe.enableTelemetry) {
213+
propertyMap.put(
214+
"platform",
215+
System.getProperty("os.name")
216+
+ " "
217+
+ System.getProperty("os.version")
218+
+ " "
219+
+ System.getProperty("os.arch"));
220+
}
216221
if (Stripe.getAppInfo() != null) {
217222
propertyMap.put("application", ApiResource.INTERNAL_GSON.toJson(Stripe.getAppInfo()));
218223
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.stripe.model;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import com.google.gson.JsonObject;
9+
import com.google.gson.JsonParser;
10+
import com.stripe.BaseStripeTest;
11+
import com.stripe.net.ApiResource;
12+
import org.junit.jupiter.api.Test;
13+
14+
public class GsonRoundTripTest extends BaseStripeTest {
15+
16+
@Test
17+
public void testUnexpandedExpandableField() {
18+
String json = "{\"id\":\"in_123\",\"object\":\"invoice\",\"customer\":\"cus_456\"}";
19+
Invoice invoice = ApiResource.GSON.fromJson(json, Invoice.class);
20+
21+
assertEquals("cus_456", invoice.getCustomer());
22+
assertNull(invoice.getCustomerObject());
23+
24+
String serialized = ApiResource.GSON.toJson(invoice);
25+
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);
26+
27+
assertEquals("cus_456", roundTripped.getCustomer());
28+
assertNull(roundTripped.getCustomerObject());
29+
}
30+
31+
@Test
32+
public void testExpandedExpandableField() {
33+
String json =
34+
"{\"id\":\"in_123\",\"object\":\"invoice\","
35+
+ "\"customer\":{\"id\":\"cus_456\",\"object\":\"customer\","
36+
+ "\"name\":\"John Doe\",\"metadata\":{\"key\":\"value\"}}}";
37+
Invoice invoice = ApiResource.GSON.fromJson(json, Invoice.class);
38+
39+
assertEquals("cus_456", invoice.getCustomer());
40+
Customer customer = invoice.getCustomerObject();
41+
assertNotNull(customer);
42+
assertEquals("cus_456", customer.getId());
43+
assertEquals("John Doe", customer.getName());
44+
45+
String serialized = ApiResource.GSON.toJson(invoice);
46+
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);
47+
48+
assertEquals("cus_456", roundTripped.getCustomer());
49+
Customer rtCustomer = roundTripped.getCustomerObject();
50+
assertNotNull(rtCustomer);
51+
assertEquals("cus_456", rtCustomer.getId());
52+
assertEquals("John Doe", rtCustomer.getName());
53+
assertEquals("value", rtCustomer.getMetadata().get("key"));
54+
}
55+
56+
@Test
57+
public void testNullExpandableField() {
58+
String json = "{\"id\":\"in_123\",\"object\":\"invoice\",\"customer\":null}";
59+
Invoice invoice = ApiResource.GSON.fromJson(json, Invoice.class);
60+
61+
assertNull(invoice.getCustomer());
62+
assertNull(invoice.getCustomerObject());
63+
64+
String serialized = ApiResource.GSON.toJson(invoice);
65+
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);
66+
67+
assertNull(roundTripped.getCustomer());
68+
assertNull(roundTripped.getCustomerObject());
69+
}
70+
71+
@Test
72+
public void testPaymentSourceDirectField() {
73+
// Charge.source is a direct PaymentSource field (not ExpandableField)
74+
String json =
75+
"{\"id\":\"ch_123\",\"object\":\"charge\","
76+
+ "\"source\":{\"id\":\"card_789\",\"object\":\"card\","
77+
+ "\"brand\":\"Visa\",\"last4\":\"4242\"}}";
78+
Charge charge = ApiResource.GSON.fromJson(json, Charge.class);
79+
80+
assertNotNull(charge.getSource());
81+
assertTrue(charge.getSource() instanceof Card);
82+
assertEquals("card_789", charge.getSource().getId());
83+
84+
String serialized = ApiResource.GSON.toJson(charge);
85+
Charge roundTripped = ApiResource.GSON.fromJson(serialized, Charge.class);
86+
87+
assertNotNull(roundTripped.getSource());
88+
assertTrue(roundTripped.getSource() instanceof Card);
89+
assertEquals("card_789", roundTripped.getSource().getId());
90+
assertEquals("Visa", ((Card) roundTripped.getSource()).getBrand());
91+
assertEquals("4242", ((Card) roundTripped.getSource()).getLast4());
92+
}
93+
94+
@Test
95+
public void testStripeRawJsonObjectRoundTrip() {
96+
String innerJson = "{\"id\":\"unknown_123\",\"object\":\"unknown_type\",\"foo\":\"bar\"}";
97+
StripeRawJsonObject raw = new StripeRawJsonObject();
98+
raw.json = JsonParser.parseString(innerJson).getAsJsonObject();
99+
100+
String serialized = ApiResource.GSON.toJson(raw);
101+
// Should serialize as the raw JSON, not wrapped in {"json":{...}}
102+
JsonObject parsed = JsonParser.parseString(serialized).getAsJsonObject();
103+
assertEquals("unknown_123", parsed.get("id").getAsString());
104+
assertEquals("bar", parsed.get("foo").getAsString());
105+
106+
StripeRawJsonObject roundTripped =
107+
ApiResource.GSON.fromJson(serialized, StripeRawJsonObject.class);
108+
assertNotNull(roundTripped.json);
109+
assertEquals("unknown_123", roundTripped.json.get("id").getAsString());
110+
assertEquals("bar", roundTripped.json.get("foo").getAsString());
111+
}
112+
113+
@Test
114+
public void testInvoiceWithExpandedCustomerRoundTrip() throws Exception {
115+
// Realistic scenario from RUN_DEVSDK-2253
116+
final String[] expansions = {"customer"};
117+
final String data = getFixture("/v1/invoices/in_123", expansions);
118+
final Invoice original = ApiResource.GSON.fromJson(data, Invoice.class);
119+
120+
assertNotNull(original.getCustomerObject());
121+
assertEquals(original.getCustomer(), original.getCustomerObject().getId());
122+
123+
String serialized = ApiResource.GSON.toJson(original);
124+
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);
125+
126+
assertEquals(original.getId(), roundTripped.getId());
127+
assertEquals(original.getCustomer(), roundTripped.getCustomer());
128+
assertNotNull(roundTripped.getCustomerObject());
129+
assertEquals(original.getCustomerObject().getId(), roundTripped.getCustomerObject().getId());
130+
}
131+
132+
@Test
133+
public void testSubscriptionWithDefaultSourceRoundTrip() throws Exception {
134+
// Realistic scenario from DEVSDK-2319
135+
final String[] expansions = {"default_source"};
136+
final String data = getFixture("/v1/subscriptions/sub_123", expansions);
137+
final Subscription original = ApiResource.GSON.fromJson(data, Subscription.class);
138+
139+
String serialized = ApiResource.GSON.toJson(original);
140+
Subscription roundTripped = ApiResource.GSON.fromJson(serialized, Subscription.class);
141+
142+
assertEquals(original.getId(), roundTripped.getId());
143+
}
144+
}

src/test/java/com/stripe/model/InvoiceTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ public void testDeserializeWithUnexpandedArrayExpansions() throws Exception {
4848
assertEquals(2, invoice.getDiscountObjects().size());
4949
}
5050

51+
@Test
52+
public void testRoundTripWithExpandedCustomer() throws Exception {
53+
final String[] expansions = {"charge", "customer"};
54+
final String data = getFixture("/v1/invoices/in_123", expansions);
55+
final Invoice original = ApiResource.GSON.fromJson(data, Invoice.class);
56+
57+
assertNotNull(original.getCustomerObject());
58+
59+
String serialized = ApiResource.GSON.toJson(original);
60+
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);
61+
62+
assertEquals(original.getId(), roundTripped.getId());
63+
assertEquals(original.getCustomer(), roundTripped.getCustomer());
64+
assertNotNull(roundTripped.getCustomerObject());
65+
assertEquals(original.getCustomerObject().getId(), roundTripped.getCustomerObject().getId());
66+
}
67+
5168
@Test
5269
public void testDeserializeWithArrayExpansions() throws Exception {
5370
final Invoice invoice =

0 commit comments

Comments
 (0)