Skip to content
Open
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
13 changes: 1 addition & 12 deletions riskified-sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,11 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.riskified</groupId>
<artifactId>riskified-sdk</artifactId>
<version>v5.0.0</version>
<version>5.0.1-SNAPSHOT</version>
<name>Riskified SDK</name>
<description>Riskified rest api SDK for java</description>
<url>https://www.riskified.com</url>

<distributionManagement>
<snapshotRepository>
<id>central</id>
<url>https://central.sonatype.com/api/v1/publisher/deployments/</url>
</snapshotRepository>
<repository>
<id>central</id>
<url>https://central.sonatype.com/api/v1/publisher/deployments/</url>
</repository>
</distributionManagement>

<build>
<plugins>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -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&lt;Address&gt; that handles backward compatibility.
*
* <p>
* Supports two JSON formats:
* <ul>
* <li><b>Current:</b> Array of addresses:
* {@code [{"address1":"..."}, ...]}</li>
* <li><b>Legacy:</b> Single address object: {@code {"address1":"..."}}</li>
* </ul>
*
* <p>
* During deserialization, single address objects are automatically wrapped in a
* list.
* Serialization always outputs the current array format.
*/
public class AddressListAdapter extends TypeAdapter<List<Address>> {
private static final Type ADDRESS_LIST_TYPE = new TypeToken<List<Address>>() {
}.getType();
private final Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();

@Override
public void write(JsonWriter out, List<Address> value) throws IOException {
gson.toJson(value, ADDRESS_LIST_TYPE, out);
}

@Override
public List<Address> 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());

}
}
Original file line number Diff line number Diff line change
@@ -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&lt;Address&gt; that handles backward compatibility.
*
* <p>
* 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.
*
* <p>
* Supports two JSON formats:
* <ul>
* <li><b>Current:</b> Array of addresses: {@code [{"address1":"..."}, ...]}</li>
* <li><b>Legacy:</b> Single address object: {@code {"address1":"..."}}</li>
* </ul>
*
* <p>
* 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<List<Address>>() {
}.getType();

@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Only handle List<Address> type
if (!type.equals(TypeToken.get(ADDRESS_LIST_TYPE))) {
return null;
}

// Return adapter that uses parent Gson configuration
return (TypeAdapter<T>) new AddressListTypeAdapter(gson);
}

/**
* TypeAdapter for List&lt;Address&gt; that uses parent Gson configuration.
*/
private static class AddressListTypeAdapter extends TypeAdapter<List<Address>> {
private final Gson gson;

AddressListTypeAdapter(Gson gson) {
this.gson = gson;
}

@Override
public void write(JsonWriter out, List<Address> value) throws IOException {
gson.toJson(value, ADDRESS_LIST_TYPE, out);
}

@Override
public List<Address> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -54,7 +56,9 @@ public abstract class BaseOrder implements IValidated {
private String vendorId;
private String vendorName;
private String vendorIntegrationType;
@JsonAdapter(AddressListAdapterFactory.class)
private List<Address> shippingAddress;
@JsonAdapter(AddressListAdapterFactory.class)
private List<Address> billingAddress;
private List<? extends IPaymentDetails> paymentDetails;
private ClientDetails clientDetails;
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}