From fa54bc375c44032790e9421a9ef99bd1af17fc76 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Thu, 30 Jul 2026 14:13:42 -0700 Subject: [PATCH 1/4] Add discriminated union serialization tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests both request-side (params → map) and response-side (JSON → object) serialization for discriminated unions, covering standalone and inline variants. Co-Authored-By: Claude Opus 4.6 Committed-By-Agent: claude --- .../DiscriminatedUnionSerializationTest.java | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java diff --git a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java new file mode 100644 index 00000000000..19c4385a8c9 --- /dev/null +++ b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java @@ -0,0 +1,269 @@ +package com.stripe.net; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.StripeObject; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class DiscriminatedUnionSerializationTest { + private final ApiRequestParamsConverter converter = new ApiRequestParamsConverter(); + + // --------------------------------------------------------------------------- + // Request-side fixtures — standalone union + // The parent params hold an Object-typed field that can hold any variant. + // --------------------------------------------------------------------------- + + @SuppressWarnings("UnusedVariable") + private static class TestCreateParams extends ApiRequestParams { + @SerializedName("color") + Object color; + + @SerializedName("name") + String name; + } + + @SuppressWarnings("UnusedVariable") + private static class TestRgbColorParams extends ApiRequestParams { + @SerializedName("model") + String model = "rgb"; + + @SerializedName("r") + Long r; + + @SerializedName("g") + Long g; + + @SerializedName("b") + Long b; + } + + @SuppressWarnings("UnusedVariable") + private static class TestHsvColorParams extends ApiRequestParams { + @SerializedName("model") + String model = "hsv"; + + @SerializedName("h") + Long h; + + @SerializedName("s") + Long s; + + @SerializedName("v") + Long v; + } + + // --------------------------------------------------------------------------- + // Request-side fixtures — inline union + // The parent params hold the discriminator and each variant's fields directly. + // --------------------------------------------------------------------------- + + @SuppressWarnings("UnusedVariable") + private static class TestInlineParams extends ApiRequestParams { + @SerializedName("type") + String type; + + @SerializedName("card") + TestCardParams card; + + @SerializedName("bank") + TestBankParams bank; + } + + @SuppressWarnings("UnusedVariable") + private static class TestCardParams extends ApiRequestParams { + @SerializedName("number") + String number; + + @SerializedName("exp_month") + Long expMonth; + } + + @SuppressWarnings("UnusedVariable") + private static class TestBankParams extends ApiRequestParams { + @SerializedName("routing_number") + String routingNumber; + + @SerializedName("account_number") + String accountNumber; + } + + // --------------------------------------------------------------------------- + // Response-side fixtures + // --------------------------------------------------------------------------- + + private static class TestColorEntity extends StripeObject { + @SerializedName("model") + String model; + + @SerializedName("r") + Long r; + + @SerializedName("g") + Long g; + + @SerializedName("b") + Long b; + + @SerializedName("h") + Long h; + + @SerializedName("s") + Long s; + + @SerializedName("v") + Long v; + } + + private static class TestColorContainer extends StripeObject { + @SerializedName("color") + TestColorEntity color; + + @SerializedName("name") + String name; + } + + private static class TestPaymentEntity extends StripeObject { + @SerializedName("type") + String type; + + @SerializedName("card") + TestCardEntity card; + + @SerializedName("bank") + TestBankEntity bank; + } + + private static class TestCardEntity extends StripeObject { + @SerializedName("number") + String number; + + @SerializedName("exp_month") + Long expMonth; + } + + private static class TestBankEntity extends StripeObject { + @SerializedName("routing_number") + String routingNumber; + + @SerializedName("account_number") + String accountNumber; + } + + // --------------------------------------------------------------------------- + // Tests — request side (params → map) + // --------------------------------------------------------------------------- + + @Test + @SuppressWarnings("unchecked") + public void testStandaloneUnion_RgbVariant_Serialization() { + TestRgbColorParams rgb = new TestRgbColorParams(); + rgb.r = 255L; + rgb.g = 128L; + rgb.b = 0L; + + TestCreateParams params = new TestCreateParams(); + params.color = rgb; + params.name = "sunset"; + + Map map = converter.convert(params); + assertEquals("sunset", map.get("name")); + + Map colorMap = (Map) map.get("color"); + assertEquals("rgb", colorMap.get("model")); + assertEquals(255L, colorMap.get("r")); + assertEquals(128L, colorMap.get("g")); + assertEquals(0L, colorMap.get("b")); + } + + @Test + @SuppressWarnings("unchecked") + public void testStandaloneUnion_HsvVariant_Serialization() { + TestHsvColorParams hsv = new TestHsvColorParams(); + hsv.h = 30L; + hsv.s = 100L; + hsv.v = 100L; + + TestCreateParams params = new TestCreateParams(); + params.color = hsv; + params.name = "orange"; + + Map map = converter.convert(params); + assertEquals("orange", map.get("name")); + + Map colorMap = (Map) map.get("color"); + assertEquals("hsv", colorMap.get("model")); + assertEquals(30L, colorMap.get("h")); + assertEquals(100L, colorMap.get("s")); + assertEquals(100L, colorMap.get("v")); + } + + @Test + @SuppressWarnings("unchecked") + public void testInlineUnion_CardVariant_Serialization() { + TestCardParams card = new TestCardParams(); + card.number = "4242424242424242"; + card.expMonth = 12L; + + TestInlineParams params = new TestInlineParams(); + params.type = "card"; + params.card = card; + + Map map = converter.convert(params); + assertEquals("card", map.get("type")); + + Map cardMap = (Map) map.get("card"); + assertEquals("4242424242424242", cardMap.get("number")); + assertEquals(12L, cardMap.get("exp_month")); + } + + @Test + @SuppressWarnings("unchecked") + public void testInlineUnion_BankVariant_Serialization() { + TestBankParams bank = new TestBankParams(); + bank.routingNumber = "110000000"; + bank.accountNumber = "000123456789"; + + TestInlineParams params = new TestInlineParams(); + params.type = "bank"; + params.bank = bank; + + Map map = converter.convert(params); + assertEquals("bank", map.get("type")); + + Map bankMap = (Map) map.get("bank"); + assertEquals("110000000", bankMap.get("routing_number")); + assertEquals("000123456789", bankMap.get("account_number")); + } + + // --------------------------------------------------------------------------- + // Tests — response side (JSON → object) + // --------------------------------------------------------------------------- + + @Test + public void testStandaloneUnion_RgbVariant_Deserialization() { + String json = + "{\"color\": {\"model\": \"rgb\", \"r\": 255, \"g\": 128, \"b\": 0}, \"name\": \"sunset\"}"; + + TestColorContainer container = ApiResource.GSON.fromJson(json, TestColorContainer.class); + + assertEquals("sunset", container.name); + assertEquals("rgb", container.color.model); + assertEquals(Long.valueOf(255L), container.color.r); + assertEquals(Long.valueOf(128L), container.color.g); + assertEquals(Long.valueOf(0L), container.color.b); + } + + @Test + public void testInlineUnion_CardVariant_Deserialization() { + String json = + "{\"type\": \"card\", \"card\": {\"number\": \"4242424242424242\", \"exp_month\": 12}}"; + + TestPaymentEntity entity = ApiResource.GSON.fromJson(json, TestPaymentEntity.class); + + assertEquals("card", entity.type); + assertEquals("4242424242424242", entity.card.number); + assertEquals(Long.valueOf(12L), entity.card.expMonth); + } +} From fd02878d9a106994dd2249fc4c64eabde3bd520b Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Fri, 7 Aug 2026 13:17:20 -0700 Subject: [PATCH 2/4] Add null assertions for non-selected DU variants Assert that when one variant is selected, the other variant fields/structs remain null in both serialization and deserialization tests. Co-Authored-By: Claude Opus 4.6 Committed-By-Agent: claude --- .../net/DiscriminatedUnionSerializationTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java index 19c4385a8c9..64727db7209 100644 --- a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java +++ b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java @@ -216,6 +216,9 @@ public void testInlineUnion_CardVariant_Serialization() { Map cardMap = (Map) map.get("card"); assertEquals("4242424242424242", cardMap.get("number")); assertEquals(12L, cardMap.get("exp_month")); + + // Non-selected variant is not present in serialized output. + assertEquals(null, map.get("bank")); } @Test @@ -253,6 +256,11 @@ public void testStandaloneUnion_RgbVariant_Deserialization() { assertEquals(Long.valueOf(255L), container.color.r); assertEquals(Long.valueOf(128L), container.color.g); assertEquals(Long.valueOf(0L), container.color.b); + + // Non-selected variant fields remain null. + assertEquals(null, container.color.h); + assertEquals(null, container.color.s); + assertEquals(null, container.color.v); } @Test @@ -265,5 +273,8 @@ public void testInlineUnion_CardVariant_Deserialization() { assertEquals("card", entity.type); assertEquals("4242424242424242", entity.card.number); assertEquals(Long.valueOf(12L), entity.card.expMonth); + + // Non-selected variant remains null. + assertEquals(null, entity.bank); } } From 1f608665c15eab23cec3bc2d3d0ca36a9cacd336 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Fri, 7 Aug 2026 18:32:58 -0700 Subject: [PATCH 3/4] Fix response standalone DU test to use proper class hierarchy Restructure the response-side test fixtures from a flat TestColorEntity class (holding all variant fields) to a proper class hierarchy: TestColorEntity (base) -> TestRgbColorEntity / TestHsvColorEntity. Add a TestColorTypeAdapterFactory that dispatches deserialization to the correct subclass based on the `model` discriminator field, mirroring how the codegen will emit TypeAdapterFactory-based dispatch for real DUs. Update assertions to use instanceof checks and typed casts rather than checking for null fields on a flat class. Add a second test covering the HSV variant path. Co-Authored-By: Claude Sonnet 4.6 Committed-By-Agent: claude --- .../DiscriminatedUnionSerializationTest.java | 96 ++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java index 64727db7209..e1086e0c23f 100644 --- a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java +++ b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java @@ -1,15 +1,33 @@ package com.stripe.net; import static org.junit.jupiter.api.Assertions.assertEquals; - +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.stripe.model.StripeObject; +import java.io.IOException; import java.util.Map; import org.junit.jupiter.api.Test; public class DiscriminatedUnionSerializationTest { private final ApiRequestParamsConverter converter = new ApiRequestParamsConverter(); + private final Gson testGson = + new GsonBuilder() + .registerTypeAdapterFactory(new TestColorTypeAdapterFactory()) + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + // --------------------------------------------------------------------------- // Request-side fixtures — standalone union // The parent params hold an Object-typed field that can hold any variant. @@ -96,7 +114,9 @@ private static class TestBankParams extends ApiRequestParams { private static class TestColorEntity extends StripeObject { @SerializedName("model") String model; + } + private static class TestRgbColorEntity extends TestColorEntity { @SerializedName("r") Long r; @@ -105,7 +125,9 @@ private static class TestColorEntity extends StripeObject { @SerializedName("b") Long b; + } + private static class TestHsvColorEntity extends TestColorEntity { @SerializedName("h") Long h; @@ -116,6 +138,45 @@ private static class TestColorEntity extends StripeObject { Long v; } + private static class TestColorTypeAdapterFactory implements TypeAdapterFactory { + @Override + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken type) { + if (!TestColorEntity.class.isAssignableFrom(type.getRawType())) { + return null; + } + return (TypeAdapter) + new TypeAdapter() { + @Override + @SuppressWarnings("unchecked") + public void write(JsonWriter out, TestColorEntity value) throws IOException { + ((TypeAdapter) gson.getAdapter(value.getClass())).write(out, value); + } + + @Override + public TestColorEntity read(JsonReader in) throws IOException { + JsonObject obj = JsonParser.parseReader(in).getAsJsonObject(); + String model = obj.has("model") ? obj.get("model").getAsString() : null; + if ("rgb".equals(model)) { + return gson + .getDelegateAdapter( + TestColorTypeAdapterFactory.this, TypeToken.get(TestRgbColorEntity.class)) + .fromJsonTree(obj); + } else if ("hsv".equals(model)) { + return gson + .getDelegateAdapter( + TestColorTypeAdapterFactory.this, TypeToken.get(TestHsvColorEntity.class)) + .fromJsonTree(obj); + } + return gson + .getDelegateAdapter( + TestColorTypeAdapterFactory.this, TypeToken.get(TestColorEntity.class)) + .fromJsonTree(obj); + } + }; + } + } + private static class TestColorContainer extends StripeObject { @SerializedName("color") TestColorEntity color; @@ -249,18 +310,31 @@ public void testStandaloneUnion_RgbVariant_Deserialization() { String json = "{\"color\": {\"model\": \"rgb\", \"r\": 255, \"g\": 128, \"b\": 0}, \"name\": \"sunset\"}"; - TestColorContainer container = ApiResource.GSON.fromJson(json, TestColorContainer.class); + TestColorContainer container = testGson.fromJson(json, TestColorContainer.class); assertEquals("sunset", container.name); - assertEquals("rgb", container.color.model); - assertEquals(Long.valueOf(255L), container.color.r); - assertEquals(Long.valueOf(128L), container.color.g); - assertEquals(Long.valueOf(0L), container.color.b); - - // Non-selected variant fields remain null. - assertEquals(null, container.color.h); - assertEquals(null, container.color.s); - assertEquals(null, container.color.v); + assertTrue(container.color instanceof TestRgbColorEntity); + TestRgbColorEntity rgb = (TestRgbColorEntity) container.color; + assertEquals("rgb", rgb.model); + assertEquals(Long.valueOf(255L), rgb.r); + assertEquals(Long.valueOf(128L), rgb.g); + assertEquals(Long.valueOf(0L), rgb.b); + } + + @Test + public void testStandaloneUnion_HsvVariant_Deserialization() { + String json = + "{\"color\": {\"model\": \"hsv\", \"h\": 30, \"s\": 100, \"v\": 50}, \"name\": \"orange\"}"; + + TestColorContainer container = testGson.fromJson(json, TestColorContainer.class); + + assertEquals("orange", container.name); + assertTrue(container.color instanceof TestHsvColorEntity); + TestHsvColorEntity hsv = (TestHsvColorEntity) container.color; + assertEquals("hsv", hsv.model); + assertEquals(Long.valueOf(30L), hsv.h); + assertEquals(Long.valueOf(100L), hsv.s); + assertEquals(Long.valueOf(50L), hsv.v); } @Test From 4d253ad0c3c7bbb22d4d2c1e2227dfb02d7f29eb Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Fri, 7 Aug 2026 19:57:38 -0700 Subject: [PATCH 4/4] fixed formatting --- .../stripe/net/DiscriminatedUnionSerializationTest.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java index e1086e0c23f..82417d7448e 100644 --- a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java +++ b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java @@ -158,18 +158,15 @@ public TestColorEntity read(JsonReader in) throws IOException { JsonObject obj = JsonParser.parseReader(in).getAsJsonObject(); String model = obj.has("model") ? obj.get("model").getAsString() : null; if ("rgb".equals(model)) { - return gson - .getDelegateAdapter( + return gson.getDelegateAdapter( TestColorTypeAdapterFactory.this, TypeToken.get(TestRgbColorEntity.class)) .fromJsonTree(obj); } else if ("hsv".equals(model)) { - return gson - .getDelegateAdapter( + return gson.getDelegateAdapter( TestColorTypeAdapterFactory.this, TypeToken.get(TestHsvColorEntity.class)) .fromJsonTree(obj); } - return gson - .getDelegateAdapter( + return gson.getDelegateAdapter( TestColorTypeAdapterFactory.this, TypeToken.get(TestColorEntity.class)) .fromJsonTree(obj); }