From 5c8b2e51f248c05a04b105e706750aa029f6dda0 Mon Sep 17 00:00:00 2001 From: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:44:30 -0800 Subject: [PATCH 1/6] don't publish private-preview javadocs (#2159) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86c0dbccc2e..38c2cf60ec2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,7 @@ jobs: ((github.event_name == 'workflow_dispatch') || (github.event_name == 'push')) && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'beta') && + !contains(github.ref, 'alpha') && endsWith(github.actor, '-stripe') needs: [build, test] runs-on: "ubuntu-24.04" From 6c2364a703d0cd0e434202cb328841a8840c23b5 Mon Sep 17 00:00:00 2001 From: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:17:43 -0800 Subject: [PATCH 2/6] add .claude directory (#2162) --- .claude/CLAUDE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .claude/CLAUDE.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000000..acfd79d1a68 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,49 @@ +# stripe-java + +## Testing + +- Run all tests: `just test` +- Run a single test class: `just test-one com.stripe.net.HttpClientTest` +- Pass extra Gradle args: `just test --tests com.stripe.SomeTest` + +## Formatting + +- Format: `just format` (uses Spotless via Gradle) +- Format check: `just format-check` + +## Key Locations + +- HTTP client abstract base (retry logic): `src/main/java/com/stripe/net/HttpClient.java` +- HTTP implementation: `src/main/java/com/stripe/net/HttpURLConnectionClient.java` +- Header management: `src/main/java/com/stripe/net/HttpHeaders.java` +- Request building: `src/main/java/com/stripe/net/StripeRequest.java` +- Authentication: `src/main/java/com/stripe/net/Authenticator.java`, `BearerTokenAuthenticator.java` +- Response getter (request dispatch): `src/main/java/com/stripe/net/LiveStripeResponseGetter.java` +- Main config/version: `src/main/java/com/stripe/Stripe.java` +- Client class: `src/main/java/com/stripe/StripeClient.java` + +## Generated Code + +- Files containing `File generated from our OpenAPI spec` at the top are generated; do not edit. Similarly, any code block starting with `The beginning of the section generated from our OpenAPI spec` is generated and should not be edited directly. + - If something in a generated file/range needs to be updated, add a summary of the change to your report but don't attempt to edit it directly. +- Resource model classes under `src/main/java/com/stripe/model/` are largely generated. +- The `net/` package (HTTP client, headers, request/response) is NOT generated. + +## Conventions + +- Uses Java's built-in `HttpURLConnection` for HTTP +- Requires JDK 17 to build +- Gradle build system +- Work is not complete until `just test` passes + +### Comments + +- Comments MUST only be used to: + 1. Document a function + 2. Explain the WHY of a piece of code + 3. Explain a particularly complicated piece of code +- Comments NEVER should be used to: + 1. Say what used to be there. That's no longer relevant! + 2. Explain the WHAT of a piece of code (unless it's very non-obvious) + +It's ok not to put comments on/in a function if their addition wouldn't meaningfully clarify anything. From ac1c6fd46d5e906b4672c256a824a58ca525e548 Mon Sep 17 00:00:00 2001 From: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:59:26 -0800 Subject: [PATCH 3/6] Add agent information to UserAgent (#2165) --- src/main/java/com/stripe/net/HttpClient.java | 40 ++++++++++++++ .../java/com/stripe/net/HttpClientTest.java | 55 +++++++++++++++++-- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/stripe/net/HttpClient.java b/src/main/java/com/stripe/net/HttpClient.java index 2d4942d9964..ebbae9e9842 100644 --- a/src/main/java/com/stripe/net/HttpClient.java +++ b/src/main/java/com/stripe/net/HttpClient.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.Properties; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; /** Base abstract class for HTTP clients used to send requests to Stripe's API. */ public abstract class HttpClient { @@ -137,12 +138,39 @@ public StripeResponseStream requestStreamWithRetries(StripeRequest request) return sendWithRetries(request, (r) -> this.requestStream(r)); } + static String detectAIAgent() { + return detectAIAgent(System::getenv); + } + + static String detectAIAgent(Function getEnv) { + String[][] agents = { + {"ANTIGRAVITY_CLI_ALIAS", "antigravity"}, + {"CLAUDECODE", "claude_code"}, + {"CLINE_ACTIVE", "cline"}, + {"CODEX_SANDBOX", "codex_cli"}, + {"CURSOR_AGENT", "cursor"}, + {"GEMINI_CLI", "gemini_cli"}, + {"OPENCODE", "open_code"}, + }; + for (String[] agent : agents) { + String val = getEnv.apply(agent[0]); + if (val != null && !val.isEmpty()) { + return agent[1]; + } + } + return ""; + } + /** * Builds the value of the {@code User-Agent} header. * * @return a string containing the value of the {@code User-Agent} header */ protected static String buildUserAgentString(StripeRequest request) { + return buildUserAgentString(request, detectAIAgent()); + } + + static String buildUserAgentString(StripeRequest request, String aiAgent) { String apiMode = request.apiMode() == ApiMode.V2 ? "v2" : "v1"; String userAgent = String.format("Stripe/%s JavaBindings/%s", apiMode, Stripe.VERSION); @@ -151,6 +179,10 @@ protected static String buildUserAgentString(StripeRequest request) { userAgent += " " + formatAppInfo(Stripe.getAppInfo()); } + if (!aiAgent.isEmpty()) { + userAgent += " AIAgent/" + aiAgent; + } + return userAgent; } @@ -160,6 +192,10 @@ protected static String buildUserAgentString(StripeRequest request) { * @return a string containing the value of the {@code X-Stripe-Client-User-Agent} header */ protected static String buildXStripeClientUserAgentString() { + return buildXStripeClientUserAgentString(detectAIAgent()); + } + + static String buildXStripeClientUserAgentString(String aiAgent) { String[] propertyNames = { "os.name", "os.version", @@ -182,6 +218,10 @@ protected static String buildXStripeClientUserAgentString() { } getGsonVersion().ifPresent(ver -> propertyMap.put("gson.version", ver)); + if (!aiAgent.isEmpty()) { + propertyMap.put("ai_agent", aiAgent); + } + return ApiResource.INTERNAL_GSON.toJson(propertyMap); } diff --git a/src/test/java/com/stripe/net/HttpClientTest.java b/src/test/java/com/stripe/net/HttpClientTest.java index a7e679b17eb..9edcb6a0717 100644 --- a/src/test/java/com/stripe/net/HttpClientTest.java +++ b/src/test/java/com/stripe/net/HttpClientTest.java @@ -192,9 +192,8 @@ public void testV1RequestSetsCorrectUserAgent() throws StripeException { RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(), ApiMode.V1); - assertEquals( - HttpClient.buildUserAgentString(request), - String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION)); + String userAgent = HttpClient.buildUserAgentString(request); + assertTrue(userAgent.startsWith(String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION))); } @Test @@ -207,8 +206,52 @@ public void testV2RequestSetsCorrectUserAgent() throws StripeException { RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(), ApiMode.V2); - assertEquals( - HttpClient.buildUserAgentString(request), - String.format("Stripe/v2 JavaBindings/%s", Stripe.VERSION)); + String userAgent = HttpClient.buildUserAgentString(request); + assertTrue(userAgent.startsWith(String.format("Stripe/v2 JavaBindings/%s", Stripe.VERSION))); + } + + @Test + public void testDetectAIAgent() { + String agent = HttpClient.detectAIAgent(key -> key.equals("CLAUDECODE") ? "1" : null); + assertEquals("claude_code", agent); + } + + @Test + public void testDetectAIAgentNoEnv() { + String agent = HttpClient.detectAIAgent(key -> null); + assertEquals("", agent); + } + + @Test + public void testDetectAIAgentFirstMatchWins() { + String agent = + HttpClient.detectAIAgent( + key -> { + if (key.equals("CURSOR_AGENT") || key.equals("OPENCODE")) return "1"; + return null; + }); + assertEquals("cursor", agent); + } + + @Test + public void testBuildUserAgentStringWithAIAgent() throws StripeException { + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.GET, + "http://example.com/get", + null, + RequestOptions.builder().setApiKey("sk_test_123").build(), + ApiMode.V1); + + String userAgent = HttpClient.buildUserAgentString(request, "cursor"); + assertTrue(userAgent.contains("AIAgent/cursor")); + } + + @Test + public void testBuildXStripeClientUserAgentStringWithAIAgent() { + String json = HttpClient.buildXStripeClientUserAgentString("cursor"); + com.google.gson.JsonObject parsed = + com.google.gson.JsonParser.parseString(json).getAsJsonObject(); + assertEquals("cursor", parsed.get("ai_agent").getAsString()); } } From f56a689ca2f629152a8243f6c5ad1088325c99db Mon Sep 17 00:00:00 2001 From: Stripe OpenAPI <105521251+stripe-openapi[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:22:01 +0000 Subject: [PATCH 4/6] Update generated code for v2189 and --- CODEGEN_VERSION | 2 +- OPENAPI_VERSION | 2 +- .../BalanceTransactionSourceTypeAdapterFactory.java | 11 +++++++---- .../model/ExternalAccountTypeAdapterFactory.java | 9 ++++++--- .../stripe/model/PaymentSourceTypeAdapterFactory.java | 9 ++++++--- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/CODEGEN_VERSION b/CODEGEN_VERSION index f784e5a017d..0f9f7f07fc5 100644 --- a/CODEGEN_VERSION +++ b/CODEGEN_VERSION @@ -1 +1 @@ -50c6e352d83d3fa675a676b64fc49aa9457ac736 \ No newline at end of file +46812bb1234aef25ae1070b402c602491ee66f8d \ No newline at end of file diff --git a/OPENAPI_VERSION b/OPENAPI_VERSION index 821385bf7c8..4c2310759e9 100644 --- a/OPENAPI_VERSION +++ b/OPENAPI_VERSION @@ -1 +1 @@ -v2187 \ No newline at end of file +v2189 \ No newline at end of file diff --git a/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java b/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java index 29e3c7c60be..fc84822c515 100644 --- a/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java +++ b/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java @@ -25,9 +25,6 @@ public TypeAdapter create(Gson gson, TypeToken type) { } final String discriminator = "object"; final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final TypeAdapter balanceTransactionSourceAdapter = - gson.getDelegateAdapter( - this, TypeToken.get(com.stripe.model.BalanceTransactionSource.class)); final TypeAdapter applicationFeeAdapter = gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ApplicationFee.class)); final TypeAdapter chargeAdapter = @@ -68,7 +65,13 @@ public TypeAdapter create(Gson gson, TypeToken type) { new TypeAdapter() { @Override public void write(JsonWriter out, BalanceTransactionSource value) throws IOException { - balanceTransactionSourceAdapter.write(out, value); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) + gson.getDelegateAdapter( + BalanceTransactionSourceTypeAdapterFactory.this, + TypeToken.get(value.getClass())); + adapter.write(out, value); } @Override diff --git a/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java b/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java index 1d24a1d21a3..a4c99067782 100644 --- a/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java +++ b/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java @@ -28,8 +28,6 @@ public TypeAdapter create(Gson gson, TypeToken type) { } final String discriminator = "object"; final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final TypeAdapter externalAccountAdapter = - gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ExternalAccount.class)); final TypeAdapter bankAccountAdapter = gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.BankAccount.class)); final TypeAdapter cardAdapter = @@ -39,7 +37,12 @@ public TypeAdapter create(Gson gson, TypeToken type) { new TypeAdapter() { @Override public void write(JsonWriter out, ExternalAccount value) throws IOException { - externalAccountAdapter.write(out, value); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) + gson.getDelegateAdapter( + ExternalAccountTypeAdapterFactory.this, TypeToken.get(value.getClass())); + adapter.write(out, value); } @Override diff --git a/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java b/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java index 309770b166e..59a32b6884f 100644 --- a/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java +++ b/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java @@ -25,8 +25,6 @@ public TypeAdapter create(Gson gson, TypeToken type) { } final String discriminator = "object"; final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final TypeAdapter paymentSourceAdapter = - gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.PaymentSource.class)); final TypeAdapter accountAdapter = gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.Account.class)); final TypeAdapter bankAccountAdapter = @@ -40,7 +38,12 @@ public TypeAdapter create(Gson gson, TypeToken type) { new TypeAdapter() { @Override public void write(JsonWriter out, PaymentSource value) throws IOException { - paymentSourceAdapter.write(out, value); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) + gson.getDelegateAdapter( + PaymentSourceTypeAdapterFactory.this, TypeToken.get(value.getClass())); + adapter.write(out, value); } @Override From 191492e44244269a6ec9b894e511c9544a73ef73 Mon Sep 17 00:00:00 2001 From: jar-stripe Date: Wed, 4 Mar 2026 14:42:58 -0800 Subject: [PATCH 5/6] Support serializing Stripe objects with ApiResource.GSON (#2168) --- ...ceTransactionSourceTypeAdapterFactory.java | 11 +- .../ExternalAccountTypeAdapterFactory.java | 9 +- .../PaymentSourceTypeAdapterFactory.java | 9 +- .../model/StripeRawJsonObjectSerializer.java | 18 +++ src/main/java/com/stripe/net/ApiResource.java | 2 + .../com/stripe/model/GsonRoundTripTest.java | 144 ++++++++++++++++++ .../java/com/stripe/model/InvoiceTest.java | 17 +++ 7 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/stripe/model/StripeRawJsonObjectSerializer.java create mode 100644 src/test/java/com/stripe/model/GsonRoundTripTest.java diff --git a/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java b/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java index 29e3c7c60be..fc84822c515 100644 --- a/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java +++ b/src/main/java/com/stripe/model/BalanceTransactionSourceTypeAdapterFactory.java @@ -25,9 +25,6 @@ public TypeAdapter create(Gson gson, TypeToken type) { } final String discriminator = "object"; final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final TypeAdapter balanceTransactionSourceAdapter = - gson.getDelegateAdapter( - this, TypeToken.get(com.stripe.model.BalanceTransactionSource.class)); final TypeAdapter applicationFeeAdapter = gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ApplicationFee.class)); final TypeAdapter chargeAdapter = @@ -68,7 +65,13 @@ public TypeAdapter create(Gson gson, TypeToken type) { new TypeAdapter() { @Override public void write(JsonWriter out, BalanceTransactionSource value) throws IOException { - balanceTransactionSourceAdapter.write(out, value); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) + gson.getDelegateAdapter( + BalanceTransactionSourceTypeAdapterFactory.this, + TypeToken.get(value.getClass())); + adapter.write(out, value); } @Override diff --git a/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java b/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java index 1d24a1d21a3..a4c99067782 100644 --- a/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java +++ b/src/main/java/com/stripe/model/ExternalAccountTypeAdapterFactory.java @@ -28,8 +28,6 @@ public TypeAdapter create(Gson gson, TypeToken type) { } final String discriminator = "object"; final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final TypeAdapter externalAccountAdapter = - gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.ExternalAccount.class)); final TypeAdapter bankAccountAdapter = gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.BankAccount.class)); final TypeAdapter cardAdapter = @@ -39,7 +37,12 @@ public TypeAdapter create(Gson gson, TypeToken type) { new TypeAdapter() { @Override public void write(JsonWriter out, ExternalAccount value) throws IOException { - externalAccountAdapter.write(out, value); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) + gson.getDelegateAdapter( + ExternalAccountTypeAdapterFactory.this, TypeToken.get(value.getClass())); + adapter.write(out, value); } @Override diff --git a/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java b/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java index 309770b166e..59a32b6884f 100644 --- a/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java +++ b/src/main/java/com/stripe/model/PaymentSourceTypeAdapterFactory.java @@ -25,8 +25,6 @@ public TypeAdapter create(Gson gson, TypeToken type) { } final String discriminator = "object"; final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final TypeAdapter paymentSourceAdapter = - gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.PaymentSource.class)); final TypeAdapter accountAdapter = gson.getDelegateAdapter(this, TypeToken.get(com.stripe.model.Account.class)); final TypeAdapter bankAccountAdapter = @@ -40,7 +38,12 @@ public TypeAdapter create(Gson gson, TypeToken type) { new TypeAdapter() { @Override public void write(JsonWriter out, PaymentSource value) throws IOException { - paymentSourceAdapter.write(out, value); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) + gson.getDelegateAdapter( + PaymentSourceTypeAdapterFactory.this, TypeToken.get(value.getClass())); + adapter.write(out, value); } @Override diff --git a/src/main/java/com/stripe/model/StripeRawJsonObjectSerializer.java b/src/main/java/com/stripe/model/StripeRawJsonObjectSerializer.java new file mode 100644 index 00000000000..b72548746d6 --- /dev/null +++ b/src/main/java/com/stripe/model/StripeRawJsonObjectSerializer.java @@ -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 { + @Override + public JsonElement serialize( + StripeRawJsonObject src, Type typeOfSrc, JsonSerializationContext context) { + if (src.json != null) { + return src.json; + } + return JsonNull.INSTANCE; + } +} diff --git a/src/main/java/com/stripe/net/ApiResource.java b/src/main/java/com/stripe/net/ApiResource.java index d23ce045463..8f6abb033a0 100644 --- a/src/main/java/com/stripe/net/ApiResource.java +++ b/src/main/java/com/stripe/net/ApiResource.java @@ -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() { diff --git a/src/test/java/com/stripe/model/GsonRoundTripTest.java b/src/test/java/com/stripe/model/GsonRoundTripTest.java new file mode 100644 index 00000000000..1e1763c5b6d --- /dev/null +++ b/src/test/java/com/stripe/model/GsonRoundTripTest.java @@ -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()); + } +} diff --git a/src/test/java/com/stripe/model/InvoiceTest.java b/src/test/java/com/stripe/model/InvoiceTest.java index f6c6a07c46b..edf74e992b1 100644 --- a/src/test/java/com/stripe/model/InvoiceTest.java +++ b/src/test/java/com/stripe/model/InvoiceTest.java @@ -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 = From 320b06ef7f2c16906ad77681ed00b9e7abcb7454 Mon Sep 17 00:00:00 2001 From: Stripe OpenAPI <105521251+stripe-openapi[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:45:14 +0000 Subject: [PATCH 6/6] Update generated code for v2189 and --- CODEGEN_VERSION | 2 +- src/main/java/com/stripe/model/v2/core/Event.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEGEN_VERSION b/CODEGEN_VERSION index 35857003ccc..58968e13793 100644 --- a/CODEGEN_VERSION +++ b/CODEGEN_VERSION @@ -1 +1 @@ -e382d3775a1654ee3c0e28a2c8e33704a1312132 \ No newline at end of file +c2fdb8b70a4520e54d461041362bece6a6df54e9 \ No newline at end of file diff --git a/src/main/java/com/stripe/model/v2/core/Event.java b/src/main/java/com/stripe/model/v2/core/Event.java index 1c9b0fed797..17d4f3dfd5a 100644 --- a/src/main/java/com/stripe/model/v2/core/Event.java +++ b/src/main/java/com/stripe/model/v2/core/Event.java @@ -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),