Customizing the Java SDK

Elevate, May 20-22-2025, Miami Beach, Florida

Serialization

The SDK uses Jackson to serialize and deserialize JSON. The default configured ObjectMapper uses some modules to correctly work with commercetools APIs. For more information, see JsonUtils.createObjectMapper(ModuleOptions).

Customization

To allow customization of ObjectMapper, the SDK uses ServiceLoader for ModuleSupplier. To register the supplied modules, add the resources/META-INF/services/io.vrap.rmf.base.client.utils.json.ModuleSupplier file to your project and include the fully qualified class name of the module supplier that you want to use.
io.vrap.rmf.base.client.utils.json.ModuleSupplier:
com.commercetools.api.json.ApiModuleSupplier
package com.commercetools.api.json;

import java.util.Optional;

import com.commercetools.api.models.cart.ReplicaCartDraft;
import com.commercetools.api.models.product.AttributeImpl;
import com.commercetools.api.models.review.Review;
import com.commercetools.api.models.type.FieldContainerImpl;
import com.commercetools.api.json.AtrributeDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;

import io.vrap.rmf.base.client.utils.json.modules.ModuleOptions;

/**
 * Module to configure the default jackson {@link com.fasterxml.jackson.databind.ObjectMapper} e.g. to deserialize attributes and custom fields
 */
public class ApiModule extends SimpleModule {
    private static final long serialVersionUID = 0L;

    public ApiModule(ModuleOptions options) {
        boolean attributeAsDateString = Boolean.parseBoolean(
            Optional.ofNullable(options.getOption(ApiModuleOptions.DESERIALIZE_DATE_ATTRIBUTE_AS_STRING))
                    .orElse(System.getProperty(ApiModuleOptions.DESERIALIZE_DATE_ATTRIBUTE_AS_STRING)));
        boolean customFieldAsDateString = Boolean
                .parseBoolean(Optional.ofNullable(options.getOption(ApiModuleOptions.DESERIALIZE_DATE_FIELD_AS_STRING))
                        .orElse(System.getProperty(ApiModuleOptions.DESERIALIZE_DATE_FIELD_AS_STRING)));
        addDeserializer(AttributeImpl.class, new AttributeDeserializer(attributeAsDateString));
        addDeserializer(FieldContainerImpl.class, new CustomFieldDeserializer(customFieldAsDateString));
        setMixInAnnotation(Review.class, ReviewMixin.class);
        setMixInAnnotation(ReplicaCartDraft.class, ReplicaCartDraftMixin.class);
    }
}

DateTime attributes

When using Date, Time and DateTime types for Product attributes or Custom Fields the SDK deserializes them as LocalDate, LocalTime and ZonedDateTime.
To deserialize them as a String, configure ObjectMapper with ModuleOptions. ApiModule also loads the configuration options by using System.getProperty(String); for example, commercetools.deserializeDateAttributeAsString).
ApiModuleOptions options = ApiModuleOptions
  .of()
  .withDateAttributeAsString(true)
  .withDateCustomFieldAsString(true);
ObjectMapper mapper = JsonUtils.createObjectMapper(options);

ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
  .withSerializer(ResponseSerializer.of(mapper))
  .build("test");

ProductVariant variant = mapper.readValue(
  stringFromResource("attributes.json"),
  ProductVariant.class
);

assertThat(variant.getAttributes()).isNotEmpty();

Map<String, Attribute> attributes = variant.withProductVariant(
  AttributeAccessor::asMap
);

assertThat(attributes.get("date").getValue())
  .isInstanceOfSatisfying(
    String.class,
    localDate -> assertThat(localDate).isEqualTo("2020-01-01")
  );
assertThat(attributes.get("time").getValue())
  .isInstanceOfSatisfying(
    String.class,
    localTime -> assertThat(localTime).isEqualTo("13:15:00.123")
  );
assertThat(attributes.get("datetime").getValue())
  .isInstanceOfSatisfying(
    String.class,
    dateTime -> assertThat(dateTime).isEqualTo("2020-01-01T13:15:00.123Z")
  );
assertThat(attributes.get("date").withAttribute(AttributeAccessor::asDate))
  .isInstanceOfSatisfying(
    LocalDate.class,
    localDate -> assertThat(localDate).isEqualTo("2020-01-01")
  );
assertThat(attributes.get("time").withAttribute(AttributeAccessor::asTime))
  .isInstanceOfSatisfying(
    LocalTime.class,
    localTime -> assertThat(localTime).isEqualTo("13:15:00.123")
  );
assertThat(
  attributes.get("datetime").withAttribute(AttributeAccessor::asDateTime)
)
  .isInstanceOfSatisfying(
    ZonedDateTime.class,
    dateTime -> assertThat(dateTime).isEqualTo("2020-01-01T13:15:00.123Z")
  );

assertThat(attributes.get("set-date").getValue())
  .asList()
  .first()
  .isInstanceOf(String.class);
assertThat(attributes.get("set-time").getValue())
  .asList()
  .first()
  .isInstanceOf(String.class);
assertThat(attributes.get("set-datetime").getValue())
  .asList()
  .first()
  .isInstanceOf(String.class);
assertThat(
  attributes.get("set-date").withAttribute(AttributeAccessor::asSetDate)
)
  .asList()
  .first()
  .isInstanceOf(LocalDate.class);
assertThat(
  attributes.get("set-time").withAttribute(AttributeAccessor::asSetTime)
)
  .asList()
  .first()
  .isInstanceOf(LocalTime.class);
assertThat(
  attributes.get("set-datetime").withAttribute(AttributeAccessor::asSetDateTime)
)
  .asList()
  .first()
  .isInstanceOf(ZonedDateTime.class);

Tune the client

Block execution

In some frameworks there is no support for asynchronous execution and so it is necessary to wait for the responses.

The client can wait for responses with the method .executeBlocking().
This method enforces a timeout for resilience and throws an ApiHttpException.
ProjectApiRoot apiRoot = createProjectClient();
Project project = apiRoot.get().executeBlocking().getBody();

Configure the underlying HTTP client

The ApiRootBuilder has create methods which allow passing a preconfigured HTTP client.
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy", 8080));
VrapHttpClient httpClient = new CtOkHttp4Client(builder -> builder.proxy(proxy)
  );

ProjectApiRoot apiRoot = ApiRootBuilder
  .of(httpClient)
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .withScopes("your-scopes")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .build("my-project");