diff --git a/riskified-sdk/pom.xml b/riskified-sdk/pom.xml index 269afe51..aab04d24 100644 --- a/riskified-sdk/pom.xml +++ b/riskified-sdk/pom.xml @@ -4,22 +4,11 @@ 4.0.0 com.riskified riskified-sdk - v5.0.0 + 5.0.1-SNAPSHOT Riskified SDK Riskified rest api SDK for java https://www.riskified.com - - - central - https://central.sonatype.com/api/v1/publisher/deployments/ - - - central - https://central.sonatype.com/api/v1/publisher/deployments/ - - - diff --git a/riskified-sdk/src/main/java/com/riskified/adapters/AddressListAdapter.java b/riskified-sdk/src/main/java/com/riskified/adapters/AddressListAdapter.java new file mode 100644 index 00000000..7ecd75a2 --- /dev/null +++ b/riskified-sdk/src/main/java/com/riskified/adapters/AddressListAdapter.java @@ -0,0 +1,60 @@ +package com.riskified.adapters; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.riskified.models.Address; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + +/** + * Gson TypeAdapter for List<Address> that handles backward compatibility. + * + *

+ * Supports two JSON formats: + *

    + *
  • Current: Array of addresses: + * {@code [{"address1":"..."}, ...]}
  • + *
  • Legacy: Single address object: {@code {"address1":"..."}}
  • + *
+ * + *

+ * During deserialization, single address objects are automatically wrapped in a + * list. + * Serialization always outputs the current array format. + */ +public class AddressListAdapter extends TypeAdapter> { + private static final Type ADDRESS_LIST_TYPE = new TypeToken>() { + }.getType(); + private final Gson gson = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + + @Override + public void write(JsonWriter out, List

value) throws IOException { + gson.toJson(value, ADDRESS_LIST_TYPE, out); + } + + @Override + public List
read(JsonReader reader) throws IOException, JsonParseException { + JsonElement element = JsonParser.parseReader(reader); + + if (element.isJsonNull()) { + return null; + } + + if (element.isJsonArray()) { + return gson.fromJson(element, ADDRESS_LIST_TYPE); + } else if (element.isJsonObject()) { + return Collections.singletonList(gson.fromJson(element, Address.class)); + } + + throw new JsonParseException( + "Expected array or object for address field, got: " + element.getClass().getSimpleName()); + + } +} diff --git a/riskified-sdk/src/main/java/com/riskified/adapters/AddressListAdapterFactory.java b/riskified-sdk/src/main/java/com/riskified/adapters/AddressListAdapterFactory.java new file mode 100644 index 00000000..ebf138d5 --- /dev/null +++ b/riskified-sdk/src/main/java/com/riskified/adapters/AddressListAdapterFactory.java @@ -0,0 +1,85 @@ +package com.riskified.adapters; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.riskified.models.Address; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + +/** + * Gson TypeAdapterFactory for List<Address> that handles backward compatibility. + * + *

+ * This factory creates TypeAdapters that inherit the parent Gson's configuration, + * including field naming policies and custom type adapters. This ensures compatibility + * with user-configured Gson instances. + * + *

+ * Supports two JSON formats: + *

    + *
  • Current: Array of addresses: {@code [{"address1":"..."}, ...]}
  • + *
  • Legacy: Single address object: {@code {"address1":"..."}}
  • + *
+ * + *

+ * During deserialization, single address objects are automatically wrapped in a list. + * Serialization always outputs the current array format. + * + * @since 5.1.0 + */ +public class AddressListAdapterFactory implements TypeAdapterFactory { + + private static final Type ADDRESS_LIST_TYPE = new TypeToken>() { + }.getType(); + + @Override + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken type) { + // Only handle List

type + if (!type.equals(TypeToken.get(ADDRESS_LIST_TYPE))) { + return null; + } + + // Return adapter that uses parent Gson configuration + return (TypeAdapter) new AddressListTypeAdapter(gson); + } + + /** + * TypeAdapter for List<Address> that uses parent Gson configuration. + */ + private static class AddressListTypeAdapter extends TypeAdapter> { + private final Gson gson; + + AddressListTypeAdapter(Gson gson) { + this.gson = gson; + } + + @Override + public void write(JsonWriter out, List
value) throws IOException { + gson.toJson(value, ADDRESS_LIST_TYPE, out); + } + + @Override + public List
read(JsonReader in) throws IOException { + JsonElement element = JsonParser.parseReader(in); + + if (element.isJsonNull()) { + return null; + } + + if (element.isJsonArray()) { + return gson.fromJson(element, ADDRESS_LIST_TYPE); + } else if (element.isJsonObject()) { + return Collections.singletonList(gson.fromJson(element, Address.class)); + } + + throw new JsonParseException( + "Expected array or object for address field, got: " + element.getClass().getSimpleName()); + } + } +} diff --git a/riskified-sdk/src/main/java/com/riskified/models/BaseOrder.java b/riskified-sdk/src/main/java/com/riskified/models/BaseOrder.java index 73d15c6c..083663fe 100644 --- a/riskified-sdk/src/main/java/com/riskified/models/BaseOrder.java +++ b/riskified-sdk/src/main/java/com/riskified/models/BaseOrder.java @@ -2,6 +2,8 @@ import java.util.*; +import com.google.gson.annotations.JsonAdapter; +import com.riskified.adapters.AddressListAdapterFactory; import com.riskified.validations.*; public abstract class BaseOrder implements IValidated { @@ -54,7 +56,9 @@ public abstract class BaseOrder implements IValidated { private String vendorId; private String vendorName; private String vendorIntegrationType; + @JsonAdapter(AddressListAdapterFactory.class) private List
shippingAddress; + @JsonAdapter(AddressListAdapterFactory.class) private List
billingAddress; private List paymentDetails; private ClientDetails clientDetails; diff --git a/riskified-sdk/src/test/java/com/riskified/adapters/AddressListAdapterTest.java b/riskified-sdk/src/test/java/com/riskified/adapters/AddressListAdapterTest.java new file mode 100644 index 00000000..d013a47f --- /dev/null +++ b/riskified-sdk/src/test/java/com/riskified/adapters/AddressListAdapterTest.java @@ -0,0 +1,194 @@ +package com.riskified.adapters; + +import com.google.gson.*; +import com.riskified.models.Address; +import com.riskified.models.Order; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * Unit tests for AddressListAdapterFactory to verify backward compatibility handling. + * Tests the factory-created adapter through Order deserialization. + */ +public class AddressListAdapterTest { + + private Gson gson; + + @Before + public void setUp() { + gson = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + } + + @Test + public void testLegacyFormatSingleObject() { + String json = "{\"id\":\"order-123\",\"email\":\"customer@example.com\"," + + "\"shipping_address\":{\"first_name\":\"John\",\"last_name\":\"Doe\"," + + "\"address1\":\"123 Main St\",\"city\":\"New York\",\"country\":\"US\",\"phone\":\"555-1234\"}}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNotNull("Shipping address should not be null", order.getShippingAddress()); + assertEquals("Shipping address should have exactly 1 element", 1, order.getShippingAddress().size()); + + Address address = order.getShippingAddress().get(0); + assertEquals("First name should match", "John", address.getFirstName()); + assertEquals("Last name should match", "Doe", address.getLastName()); + assertEquals("Address1 should match", "123 Main St", address.getAddress1()); + assertEquals("City should match", "New York", address.getCity()); + assertEquals("Country should match", "US", address.getCountry()); + assertEquals("Phone should match", "555-1234", address.getPhone()); + } + + @Test + public void testCurrentFormatArray() { + String json = "{\"id\":\"order-456\",\"email\":\"customer@example.com\"," + + "\"shipping_address\":[{\"first_name\":\"Jane\",\"last_name\":\"Smith\"," + + "\"address1\":\"456 Oak Ave\",\"city\":\"Los Angeles\",\"country\":\"US\",\"phone\":\"555-5678\"}]}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNotNull("Shipping address should not be null", order.getShippingAddress()); + assertEquals("Shipping address should have exactly 1 element", 1, order.getShippingAddress().size()); + + Address address = order.getShippingAddress().get(0); + assertEquals("First name should match", "Jane", address.getFirstName()); + assertEquals("Last name should match", "Smith", address.getLastName()); + assertEquals("Address1 should match", "456 Oak Ave", address.getAddress1()); + assertEquals("City should match", "Los Angeles", address.getCity()); + assertEquals("Country should match", "US", address.getCountry()); + assertEquals("Phone should match", "555-5678", address.getPhone()); + } + + @Test + public void testCurrentFormatMultipleAddresses() { + String json = "{\"id\":\"order-789\",\"email\":\"customer@example.com\"," + + "\"shipping_address\":[" + + "{\"first_name\":\"Alice\",\"last_name\":\"Johnson\",\"address1\":\"789 Pine Rd\",\"city\":\"Chicago\",\"country\":\"US\",\"phone\":\"555-1111\"}," + + "{\"first_name\":\"Bob\",\"last_name\":\"Williams\",\"address1\":\"321 Elm St\",\"city\":\"Boston\",\"country\":\"US\",\"phone\":\"555-2222\"}" + + "]}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNotNull("Shipping address should not be null", order.getShippingAddress()); + assertEquals("Shipping address should have exactly 2 elements", 2, order.getShippingAddress().size()); + + Address address1 = order.getShippingAddress().get(0); + assertEquals("First address first name should match", "Alice", address1.getFirstName()); + assertEquals("First address address1 should match", "789 Pine Rd", address1.getAddress1()); + + Address address2 = order.getShippingAddress().get(1); + assertEquals("Second address first name should match", "Bob", address2.getFirstName()); + assertEquals("Second address address1 should match", "321 Elm St", address2.getAddress1()); + } + + @Test + public void testNullShippingAddress() { + String json = "{\"id\":\"order-999\",\"email\":\"customer@example.com\",\"shipping_address\":null}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNull("Shipping address should be null", order.getShippingAddress()); + } + + @Test + public void testMissingShippingAddress() { + String json = "{\"id\":\"order-888\",\"email\":\"customer@example.com\"}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNull("Shipping address should be null when field is missing", order.getShippingAddress()); + } + + @Test + public void testMixedShippingAndbillingAddresses() { + String json = "{\"id\":\"order-777\",\"email\":\"customer@example.com\"," + + "\"shipping_address\":{\"first_name\":\"Charlie\",\"last_name\":\"Brown\",\"address1\":\"111 Maple Dr\",\"city\":\"Seattle\",\"country\":\"US\",\"phone\":\"555-3333\"}," + + "\"billing_address\":[{\"first_name\":\"Diana\",\"last_name\":\"Davis\",\"address1\":\"222 Birch Ln\",\"city\":\"Portland\",\"country\":\"US\",\"phone\":\"555-4444\"}]}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + + assertNotNull("Shipping address should not be null", order.getShippingAddress()); + assertEquals("Shipping address should have exactly 1 element", 1, order.getShippingAddress().size()); + assertEquals("Shipping address first name should match", "Charlie", order.getShippingAddress().get(0).getFirstName()); + + assertNotNull("Billing address should not be null", order.getBillingAddress()); + assertEquals("Billing address should have exactly 1 element", 1, order.getBillingAddress().size()); + assertEquals("Billing address first name should match", "Diana", order.getBillingAddress().get(0).getFirstName()); + } + + @Test + public void testRoundTripSerialization() { + Order order = new Order(); + order.setId("order-555"); + order.setEmail("test@example.com"); + + Address address = new Address("Test", "User", "999 Test St", "Testville", "555-9999", "US"); + + order.setShippingAddress(Arrays.asList(address)); + + String json = gson.toJson(order); + + assertTrue("Serialized JSON should contain shipping_address as array", json.contains("\"shipping_address\":[{")); + assertFalse("Serialized JSON should NOT have shipping_address as direct object", + json.matches(".*\"shipping_address\":\\{\"first_name\".*")); + + Order deserialized = gson.fromJson(json, Order.class); + + assertNotNull("Deserialized order should not be null", deserialized); + assertNotNull("Deserialized shipping address should not be null", deserialized.getShippingAddress()); + assertEquals("Deserialized shipping address should have 1 element", 1, deserialized.getShippingAddress().size()); + assertEquals("First name should match after round-trip", "Test", deserialized.getShippingAddress().get(0).getFirstName()); + } + + @Test(expected = JsonParseException.class) + public void testInvalidTypeThrowsException() { + String json = "{\"id\":\"order-666\",\"email\":\"customer@example.com\",\"shipping_address\":\"invalid\"}"; + + // This should throw JsonParseException due to invalid format + gson.fromJson(json, Order.class); + } + + @Test(expected = JsonParseException.class) + public void testInvalidNumberTypeThrowsException() { + String json = "{\"id\":\"order-333\",\"email\":\"customer@example.com\",\"shipping_address\":12345}"; + + // This should throw JsonParseException due to invalid format + gson.fromJson(json, Order.class); + } + + @Test + public void testEmptyArray() { + String json = "{\"id\":\"order-444\",\"email\":\"customer@example.com\",\"shipping_address\":[]}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNotNull("Shipping address should not be null", order.getShippingAddress()); + assertEquals("Shipping address should be empty list", 0, order.getShippingAddress().size()); + } + + @Test + public void testEmptyObjectInArray() { + String json = "{\"id\":\"order-222\",\"email\":\"customer@example.com\",\"shipping_address\":[{}]}"; + + Order order = gson.fromJson(json, Order.class); + + assertNotNull("Order should not be null", order); + assertNotNull("Shipping address should not be null", order.getShippingAddress()); + assertEquals("Shipping address should have 1 element", 1, order.getShippingAddress().size()); + assertNotNull("Address object should not be null", order.getShippingAddress().get(0)); + } +}