Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ This release uses the API version `2026-01-28.preview`.
* Add support for `url` on `financialconnections.Session`
* Add support for `billingCycleAnchor` on `SubscriptionCreateParams.trial_settings.end_behavior` and `SubscriptionUpdateParams.trial_settings.end_behavior`

## 31.4.1 - 2026-03-06
* [#2168](https://github.com/stripe/stripe-java/pull/2168) Support serializing Stripe objects with ApiResource.GSON
* `ApiResource.GSON` now supports serializing Stripe objects back into compatible JSON
* [#2165](https://github.com/stripe/stripe-java/pull/2165) Add AI Agent information to UserAgent

## 31.4.0 - 2026-02-25
This release changes the pinned API version to `2026-02-25.clover`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
}
final String discriminator = "object";
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
final TypeAdapter<com.stripe.model.BalanceTransactionSource> balanceTransactionSourceAdapter =
gson.getDelegateAdapter(
this, TypeToken.get(com.stripe.model.BalanceTransactionSource.class));
final TypeAdapter<com.stripe.model.ApplicationFee> applicationFeeAdapter =
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ApplicationFee.class));
final TypeAdapter<com.stripe.model.Charge> chargeAdapter =
Expand Down Expand Up @@ -68,7 +65,13 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
new TypeAdapter<BalanceTransactionSource>() {
@Override
public void write(JsonWriter out, BalanceTransactionSource value) throws IOException {
balanceTransactionSourceAdapter.write(out, value);
@SuppressWarnings("unchecked")
TypeAdapter<BalanceTransactionSource> adapter =
(TypeAdapter<BalanceTransactionSource>)
gson.getDelegateAdapter(
BalanceTransactionSourceTypeAdapterFactory.this,
TypeToken.get(value.getClass()));
adapter.write(out, value);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
}
final String discriminator = "object";
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
final TypeAdapter<com.stripe.model.ExternalAccount> externalAccountAdapter =
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ExternalAccount.class));
final TypeAdapter<com.stripe.model.BankAccount> bankAccountAdapter =
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.BankAccount.class));
final TypeAdapter<com.stripe.model.Card> cardAdapter =
Expand All @@ -39,7 +37,12 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
new TypeAdapter<ExternalAccount>() {
@Override
public void write(JsonWriter out, ExternalAccount value) throws IOException {
externalAccountAdapter.write(out, value);
@SuppressWarnings("unchecked")
TypeAdapter<ExternalAccount> adapter =
(TypeAdapter<ExternalAccount>)
gson.getDelegateAdapter(
ExternalAccountTypeAdapterFactory.this, TypeToken.get(value.getClass()));
adapter.write(out, value);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
}
final String discriminator = "object";
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
final TypeAdapter<com.stripe.model.PaymentSource> paymentSourceAdapter =
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.PaymentSource.class));
final TypeAdapter<com.stripe.model.Account> accountAdapter =
gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.Account.class));
final TypeAdapter<com.stripe.model.BankAccount> bankAccountAdapter =
Expand All @@ -40,7 +38,12 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
new TypeAdapter<PaymentSource>() {
@Override
public void write(JsonWriter out, PaymentSource value) throws IOException {
paymentSourceAdapter.write(out, value);
@SuppressWarnings("unchecked")
TypeAdapter<PaymentSource> adapter =
(TypeAdapter<PaymentSource>)
gson.getDelegateAdapter(
PaymentSourceTypeAdapterFactory.this, TypeToken.get(value.getClass()));
adapter.write(out, value);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.stripe.model;

import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;

public class StripeRawJsonObjectSerializer implements JsonSerializer<StripeRawJsonObject> {
@Override
public JsonElement serialize(
StripeRawJsonObject src, Type typeOfSrc, JsonSerializationContext context) {
if (src.json != null) {
return src.json;
}
return JsonNull.INSTANCE;
}
}
7 changes: 5 additions & 2 deletions src/main/java/com/stripe/model/v2/core/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,15 @@ protected StripeObject fetchRelatedObject(RelatedObject relatedObject) throws St
objectClass = StripeRawJsonObject.class;
}

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

if (context != null) {
opts = new RequestOptions.RequestOptionsBuilder().setStripeAccount(context).build();
optsBuilder.setStripeAccount(context);
}

RequestOptions opts = optsBuilder.build();

return this.responseGetter.request(
new ApiRequest(
BaseAddress.API, ApiResource.RequestMethod.GET, relatedObject.getUrl(), null, opts),
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/stripe/net/ApiResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ private static Gson createGson(boolean shouldSetResponseGetter) {
.registerTypeAdapter(Event.Request.class, new EventRequestDeserializer())
.registerTypeAdapter(StripeContext.class, new StripeContextDeserializer())
.registerTypeAdapter(ExpandableField.class, new ExpandableFieldDeserializer())
.registerTypeAdapter(ExpandableField.class, new ExpandableFieldSerializer())
.registerTypeAdapter(Instant.class, new InstantDeserializer())
.registerTypeAdapterFactory(new EventTypeAdapterFactory())
.registerTypeAdapter(StripeRawJsonObject.class, new StripeRawJsonObjectDeserializer())
.registerTypeAdapter(StripeRawJsonObject.class, new StripeRawJsonObjectSerializer())
.registerTypeAdapterFactory(new StripeCollectionItemTypeSettingFactory())
.addReflectionAccessFilter(
new ReflectionAccessFilter() {
Expand Down
25 changes: 15 additions & 10 deletions src/main/java/com/stripe/net/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,18 @@ static String detectAIAgent() {

static String detectAIAgent(Function<String, String> getEnv) {
String[][] agents = {
// The beginning of the section generated from our OpenAPI spec
{"ANTIGRAVITY_CLI_ALIAS", "antigravity"},
{"CLAUDECODE", "claude_code"},
{"CLINE_ACTIVE", "cline"},
{"CODEX_SANDBOX", "codex_cli"},
{"CODEX_THREAD_ID", "codex_cli"},
{"CODEX_SANDBOX_NETWORK_DISABLED", "codex_cli"},
{"CODEX_CI", "codex_cli"},
{"CURSOR_AGENT", "cursor"},
{"GEMINI_CLI", "gemini_cli"},
{"OPENCODE", "open_code"},
// The end of the section generated from our OpenAPI spec
};
for (String[] agent : agents) {
String val = getEnv.apply(agent[0]);
Expand Down Expand Up @@ -196,23 +201,23 @@ protected static String buildXStripeClientUserAgentString() {
}

static String buildXStripeClientUserAgentString(String aiAgent) {
String[] propertyNames = {
"os.name",
"os.version",
"os.arch",
"java.version",
"java.vendor",
"java.vm.version",
"java.vm.vendor"
};
String[] propertyNames = {"java.version", "java.vendor", "java.vm.version", "java.vm.vendor"};

Map<String, String> propertyMap = new HashMap<>();
for (String propertyName : propertyNames) {
propertyMap.put(propertyName, System.getProperty(propertyName));
}
propertyMap.put("bindings.version", Stripe.VERSION);
propertyMap.put("lang", "Java");
propertyMap.put("publisher", "Stripe");
if (Stripe.enableTelemetry) {
propertyMap.put(
"platform",
System.getProperty("os.name")
+ " "
+ System.getProperty("os.version")
+ " "
+ System.getProperty("os.arch"));
}
if (Stripe.getAppInfo() != null) {
propertyMap.put("application", ApiResource.INTERNAL_GSON.toJson(Stripe.getAppInfo()));
}
Expand Down
144 changes: 144 additions & 0 deletions src/test/java/com/stripe/model/GsonRoundTripTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.stripe.model;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.stripe.BaseStripeTest;
import com.stripe.net.ApiResource;
import org.junit.jupiter.api.Test;

public class GsonRoundTripTest extends BaseStripeTest {

@Test
public void testUnexpandedExpandableField() {
String json = "{\"id\":\"in_123\",\"object\":\"invoice\",\"customer\":\"cus_456\"}";
Invoice invoice = ApiResource.GSON.fromJson(json, Invoice.class);

assertEquals("cus_456", invoice.getCustomer());
assertNull(invoice.getCustomerObject());

String serialized = ApiResource.GSON.toJson(invoice);
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);

assertEquals("cus_456", roundTripped.getCustomer());
assertNull(roundTripped.getCustomerObject());
}

@Test
public void testExpandedExpandableField() {
String json =
"{\"id\":\"in_123\",\"object\":\"invoice\","
+ "\"customer\":{\"id\":\"cus_456\",\"object\":\"customer\","
+ "\"name\":\"John Doe\",\"metadata\":{\"key\":\"value\"}}}";
Invoice invoice = ApiResource.GSON.fromJson(json, Invoice.class);

assertEquals("cus_456", invoice.getCustomer());
Customer customer = invoice.getCustomerObject();
assertNotNull(customer);
assertEquals("cus_456", customer.getId());
assertEquals("John Doe", customer.getName());

String serialized = ApiResource.GSON.toJson(invoice);
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);

assertEquals("cus_456", roundTripped.getCustomer());
Customer rtCustomer = roundTripped.getCustomerObject();
assertNotNull(rtCustomer);
assertEquals("cus_456", rtCustomer.getId());
assertEquals("John Doe", rtCustomer.getName());
assertEquals("value", rtCustomer.getMetadata().get("key"));
}

@Test
public void testNullExpandableField() {
String json = "{\"id\":\"in_123\",\"object\":\"invoice\",\"customer\":null}";
Invoice invoice = ApiResource.GSON.fromJson(json, Invoice.class);

assertNull(invoice.getCustomer());
assertNull(invoice.getCustomerObject());

String serialized = ApiResource.GSON.toJson(invoice);
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);

assertNull(roundTripped.getCustomer());
assertNull(roundTripped.getCustomerObject());
}

@Test
public void testPaymentSourceDirectField() {
// Charge.source is a direct PaymentSource field (not ExpandableField)
String json =
"{\"id\":\"ch_123\",\"object\":\"charge\","
+ "\"source\":{\"id\":\"card_789\",\"object\":\"card\","
+ "\"brand\":\"Visa\",\"last4\":\"4242\"}}";
Charge charge = ApiResource.GSON.fromJson(json, Charge.class);

assertNotNull(charge.getSource());
assertTrue(charge.getSource() instanceof Card);
assertEquals("card_789", charge.getSource().getId());

String serialized = ApiResource.GSON.toJson(charge);
Charge roundTripped = ApiResource.GSON.fromJson(serialized, Charge.class);

assertNotNull(roundTripped.getSource());
assertTrue(roundTripped.getSource() instanceof Card);
assertEquals("card_789", roundTripped.getSource().getId());
assertEquals("Visa", ((Card) roundTripped.getSource()).getBrand());
assertEquals("4242", ((Card) roundTripped.getSource()).getLast4());
}

@Test
public void testStripeRawJsonObjectRoundTrip() {
String innerJson = "{\"id\":\"unknown_123\",\"object\":\"unknown_type\",\"foo\":\"bar\"}";
StripeRawJsonObject raw = new StripeRawJsonObject();
raw.json = JsonParser.parseString(innerJson).getAsJsonObject();

String serialized = ApiResource.GSON.toJson(raw);
// Should serialize as the raw JSON, not wrapped in {"json":{...}}
JsonObject parsed = JsonParser.parseString(serialized).getAsJsonObject();
assertEquals("unknown_123", parsed.get("id").getAsString());
assertEquals("bar", parsed.get("foo").getAsString());

StripeRawJsonObject roundTripped =
ApiResource.GSON.fromJson(serialized, StripeRawJsonObject.class);
assertNotNull(roundTripped.json);
assertEquals("unknown_123", roundTripped.json.get("id").getAsString());
assertEquals("bar", roundTripped.json.get("foo").getAsString());
}

@Test
public void testInvoiceWithExpandedCustomerRoundTrip() throws Exception {
// Realistic scenario from RUN_DEVSDK-2253
final String[] expansions = {"customer"};
final String data = getFixture("/v1/invoices/in_123", expansions);
final Invoice original = ApiResource.GSON.fromJson(data, Invoice.class);

assertNotNull(original.getCustomerObject());
assertEquals(original.getCustomer(), original.getCustomerObject().getId());

String serialized = ApiResource.GSON.toJson(original);
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);

assertEquals(original.getId(), roundTripped.getId());
assertEquals(original.getCustomer(), roundTripped.getCustomer());
assertNotNull(roundTripped.getCustomerObject());
assertEquals(original.getCustomerObject().getId(), roundTripped.getCustomerObject().getId());
}

@Test
public void testSubscriptionWithDefaultSourceRoundTrip() throws Exception {
// Realistic scenario from DEVSDK-2319
final String[] expansions = {"default_source"};
final String data = getFixture("/v1/subscriptions/sub_123", expansions);
final Subscription original = ApiResource.GSON.fromJson(data, Subscription.class);

String serialized = ApiResource.GSON.toJson(original);
Subscription roundTripped = ApiResource.GSON.fromJson(serialized, Subscription.class);

assertEquals(original.getId(), roundTripped.getId());
}
}
17 changes: 17 additions & 0 deletions src/test/java/com/stripe/model/InvoiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ public void testDeserializeWithUnexpandedArrayExpansions() throws Exception {
assertEquals(2, invoice.getDiscountObjects().size());
}

@Test
public void testRoundTripWithExpandedCustomer() throws Exception {
final String[] expansions = {"charge", "customer"};
final String data = getFixture("/v1/invoices/in_123", expansions);
final Invoice original = ApiResource.GSON.fromJson(data, Invoice.class);

assertNotNull(original.getCustomerObject());

String serialized = ApiResource.GSON.toJson(original);
Invoice roundTripped = ApiResource.GSON.fromJson(serialized, Invoice.class);

assertEquals(original.getId(), roundTripped.getId());
assertEquals(original.getCustomer(), roundTripped.getCustomer());
assertNotNull(roundTripped.getCustomerObject());
assertEquals(original.getCustomerObject().getId(), roundTripped.getCustomerObject().getId());
}

@Test
public void testDeserializeWithArrayExpansions() throws Exception {
final Invoice invoice =
Expand Down
41 changes: 41 additions & 0 deletions src/test/java/com/stripe/net/HttpClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,45 @@ public void testBuildXStripeClientUserAgentStringWithAIAgent() {
com.google.gson.JsonParser.parseString(json).getAsJsonObject();
assertEquals("cursor", parsed.get("ai_agent").getAsString());
}

@Test
public void testBuildXStripeClientUserAgentStringOmitsPublisherAndOsKeys() {
String json = HttpClient.buildXStripeClientUserAgentString("");
com.google.gson.JsonObject parsed =
com.google.gson.JsonParser.parseString(json).getAsJsonObject();
assertTrue(!parsed.has("publisher"));
assertTrue(!parsed.has("os.name"));
assertTrue(!parsed.has("os.version"));
assertTrue(!parsed.has("os.arch"));
}

@Test
public void testBuildXStripeClientUserAgentStringPlatformWithTelemetry() {
boolean originalTelemetry = Stripe.enableTelemetry;
try {
Stripe.enableTelemetry = true;
String json = HttpClient.buildXStripeClientUserAgentString("");
com.google.gson.JsonObject parsed =
com.google.gson.JsonParser.parseString(json).getAsJsonObject();
assertTrue(parsed.has("platform"));
String platform = parsed.get("platform").getAsString();
assertTrue(platform.contains(System.getProperty("os.name")));
} finally {
Stripe.enableTelemetry = originalTelemetry;
}
}

@Test
public void testBuildXStripeClientUserAgentStringNoPlatformWithoutTelemetry() {
boolean originalTelemetry = Stripe.enableTelemetry;
try {
Stripe.enableTelemetry = false;
String json = HttpClient.buildXStripeClientUserAgentString("");
com.google.gson.JsonObject parsed =
com.google.gson.JsonParser.parseString(json).getAsJsonObject();
assertTrue(!parsed.has("platform"));
} finally {
Stripe.enableTelemetry = originalTelemetry;
}
}
}