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 =