# Customizing the Java SDK ## Serialization The SDK uses [Jackson](https://github.com/FasterXML/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)](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/utils/json/JsonUtils.html#createObjectMapper\(io.vrap.rmf.base.client.utils.json.modules.ModuleOptions\)). ### Customization To allow customization of `ObjectMapper`, the SDK uses [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) for [ModuleSupplier](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/utils/json/ModuleSupplier.html). 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. ```java title="Register the custom ModuleSupplier" io.vrap.rmf.base.client.utils.json.ModuleSupplier: com.commercetools.api.json.ApiModuleSupplier ``` ```java title="Implement a custom ApiModule" 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.AttributeDeserializer; 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 Attributes or Custom Fields the SDK deserializes them as [LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html), [LocalTime](https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html) and [ZonedDateTime](https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html). To deserialize them as a String, configure ObjectMapper with [ModuleOptions](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/utils/json/modules/ModuleOptions.html). [ApiModule](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/json/ApiModule.html) also loads the configuration options by using `System.getProperty(String)`; for example, [commercetools.deserializeDateAttributeAsString](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/json/ApiModuleOptions.html#DESERIALIZE_DATE_ATTRIBUTE_AS_STRING)). ```java title="Deserialize Date, Time, and DateTime Attributes as strings" 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 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); ``` ### Number attributes When using number types for Attributes or Custom Fields, the SDK automatically deserializes them as [Long](https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html) if they have no fractional part. To always deserialize them as [Double](https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html), configure `ObjectMapper` with [ApiModuleOptions](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/json/ApiModuleOptions.html): ```java title="Deserialize number Attributes as Double" ApiModuleOptions options = ApiModuleOptions .of() .withAttributeNumberAsDouble(true) .withCustomFieldNumberAsDouble(true); ObjectMapper mapper = JsonUtils.createObjectMapper(options); ``` You can also set these options using system properties: - `commercetools.deserializeAttributeNumberAsDouble` - `commercetools.deserializeCustomFieldNumberAsDouble` ### Deserialize Attributes as JsonNode If the automatic deserialization of Attributes or Custom Fields is not needed, you can configure the SDK to deserialize them as a [JsonNode](https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/JsonNode.html) instead: ```java title="Deserialize Attributes as JsonNode" ApiModuleOptions options = ApiModuleOptions .of() .withAttributeAsJsonNode(true) .withCustomFieldAsJsonNode(true); ObjectMapper mapper = JsonUtils.createObjectMapper(options); ``` You can also set these options using system properties: - `commercetools.deserializeAttributeAsJsonNode` - `commercetools.deserializeCustomFieldAsJsonNode` ## Authentication The Java SDK supports multiple authentication flows. The `defaultClient()` method on [ApiRootBuilder](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/defaultconfig/ApiRootBuilder.html) configures the client credentials flow by default. ### Static token When you want to use an existing token (for example, one provided by an external OAuth service), use the `withStaticTokenFlow()` method: ```java title="Configure a client with a static token" AuthenticationToken token = AuthenticationToken .builder() .accessToken("your-existing-token") .build(); ProjectApiRoot apiRoot = ApiRootBuilder .of() .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()) .withStaticTokenFlow(token) .build("my-project-key"); ``` ### Anonymous session and refresh token flow The `withAnonymousRefreshFlow()` method configures a stack of token providers that first try to retrieve a token from a [TokenStorage](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/oauth2/TokenStorage.html). If no token exists, it requests one using the anonymous token flow. If the token is expired, the [RefreshFlowTokenSupplier](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/oauth2/RefreshFlowTokenSupplier.html) tries to refresh it. This can be combined with the [GlobalCustomerPasswordTokenSupplier](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/oauth2/GlobalCustomerPasswordTokenSupplier.html) to request a customer-bound token and save it in the `TokenStorage`. ```java title="Configure the anonymous refresh token flow" ProjectApiRoot apiRoot = ApiRootBuilder .of() .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()) .withAnonymousRefreshFlow( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), ServiceRegion.GCP_EUROPE_WEST1, tokenStorage ) .build("my-project-key"); ``` ### Anonymous session flow The [AnonymousSessionTokenSupplier](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/oauth2/AnonymousSessionTokenSupplier.html) requests an anonymous token without a fallback to a refresh token flow. Configure it with `withAnonymousSessionFlow()`: ```java title="Configure the anonymous session flow" ProjectApiRoot apiRoot = ApiRootBuilder .of() .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()) .withAnonymousSessionFlow( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), "anonymous-id" ) .build("my-project-key"); ``` ### Password flow Use `withGlobalCustomerPasswordFlow()` to configure a client with a token issued to a specific customer using the [GlobalCustomerPasswordTokenSupplier](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/oauth2/GlobalCustomerPasswordTokenSupplier.html): ```java title="Configure the customer password flow" ProjectApiRoot apiRoot = ApiRootBuilder .of() .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()) .withGlobalCustomerPasswordFlow( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), "customer@example.com", "customer-password", "my-project-key" ) .build("my-project-key"); ``` ### Token introspection [Token introspection](/api/authorization.md#introspection) is not available through the SDK client directly. You must call the introspection endpoint using a raw `ApiHttpClient`: ```java title="Call the token introspection endpoint" ClientCredentials credentials = ClientCredentials.of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(); // Obtain a token ClientCredentialsTokenSupplier tokenSupplier = new ClientCredentialsTokenSupplier( credentials.getClientId(), credentials.getClientSecret(), "", ServiceRegion.GCP_EUROPE_WEST1.getOAuthTokenUrl(), HttpClientSupplier.of().get() ); final AuthenticationToken token = tokenSupplier.getToken().get(); // Create a client targeted at the auth endpoint ApiHttpClient client = ClientBuilder.of() .defaultClient(ServiceRegion.GCP_EUROPE_WEST1.getAuthUrl()) .build(); // Build Basic auth header final String auth = Base64.getEncoder().encodeToString( (credentials.getClientId() + ":" + credentials.getClientSecret()) .getBytes(StandardCharsets.UTF_8)); final ApiHttpHeaders headers = new ApiHttpHeaders() .withHeader(ApiHttpHeaders.AUTHORIZATION, format("Basic %s", auth)) .withHeader(ApiHttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded"); // Call the introspection endpoint final ApiHttpRequest request = new ApiHttpRequest( ApiHttpMethod.POST, URI.create(ServiceRegion.GCP_EUROPE_WEST1.getAuthUrl() + "/oauth/introspect"), headers, ("token=" + token.getAccessToken()).getBytes(StandardCharsets.UTF_8)); final ApiHttpResponse result = client.execute(request) .thenApply(r -> client.getSerializerService() .convertResponse(r, TokenIntrospection.class)) .get(); System.out.println("Token active: " + result.getBody().isActive()); ``` ### Token revocation [Token revocation](/api/authorization.md#revoke-tokens) also requires calling the endpoint directly. Use the same credential and header setup as for introspection: ```java title="Call the token revocation endpoint" // Build the revoke request body final String revokeBody = "token=" + token.getAccessToken() + "&token_type_hint=access_token"; final ApiHttpRequest revokeRequest = new ApiHttpRequest( ApiHttpMethod.POST, URI.create(ServiceRegion.GCP_EUROPE_WEST1.getAuthUrl() + "/oauth/token/revoke"), headers, revokeBody.getBytes(StandardCharsets.UTF_8)); // A successful revocation returns HTTP 200 final ApiHttpResponse revokeToken = client.execute(revokeRequest).get(); ``` ## 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`. ```java title="Execute a request synchronously" 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. ```java title="Configure an HTTP client with a proxy" 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"); ``` ### Custom URLs To use custom URLs for API endpoints and authentication, provide the base URIs to the `defaultClient()` method: ```java title="Configure custom API and authentication URLs" ProjectApiRoot apiRoot = ApiRootBuilder .of() .defaultClient( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), URI.create("https://auth.custom.example.com/oauth/token"), URI.create("https://api.custom.example.com") ) .build("my-project-key"); ``` ### Not-found middleware By default, [ErrorMiddleware](/dev-tooling/java-sdk-middleware.md#errormiddleware) throws an exception for any response with a status code of `400` or higher, including `404` Not Found. To return `null` instead of throwing a [NotFoundException](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/error/NotFoundException.html) for `404` responses, use `addNotFoundExceptionMiddleware()`: ```java title="Return null instead of throwing for 404 responses" ProjectApiRoot apiRoot = ApiRootBuilder .of() .defaultClient( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), ServiceRegion.GCP_EUROPE_WEST1 ) .addNotFoundExceptionMiddleware() .build("my-project-key"); ``` You can also filter which requests use this behavior by passing a predicate to the method. ### Limit concurrent requests By default the client is initialized with a [ForkJoinPool](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html). The underlying HTTP client (OkHttp and Apache AsyncHTTP) is configured for a maximum of 64 concurrent requests by default. To adjust the maximum concurrent requests, instantiate the HTTP client directly: ```java title="Limit concurrent requests with a custom thread pool" VrapHttpClient httpClient = new CtOkHttp4Client( builder -> builder.dispatcher( new Dispatcher(Executors.newFixedThreadPool(20)) ) ); ProjectApiRoot apiRoot = ApiRootBuilder .of(httpClient) .defaultClient( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), ServiceRegion.GCP_EUROPE_WEST1 ) .build("my-project-key"); ``` Alternatively, use [QueueMiddleware](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/http/QueueMiddleware.html) to limit concurrent requests at the middleware level: ```java title="Limit concurrent requests with QueueMiddleware" ProjectApiRoot apiRoot = ApiRootBuilder .of() .defaultClient( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), ServiceRegion.GCP_EUROPE_WEST1 ) .addMiddleware(new QueueMiddleware(20)) .build("my-project-key"); ``` ### Timeouts To build resilient applications, it's essential to configure timeouts and retries correctly. The underlying HTTP clients are configured to timeout after 120 seconds by default. You can take several approaches to limiting the time a request can take. For guidance about timeout values, see [Error handling through timeout and retries](/api/error-handling.md) #### Middleware-based timeout The most flexible approach is to use a middleware that times out when needed. This lets you apply different timeouts to different types of requests. The [failsafe](https://failsafe.dev/) library is recommended for implementing this: ```java title="Configure a middleware-based timeout" Timeout> timeout = Timeout .>builder(Duration.ofSeconds(10)) .build(); FailsafeExecutor> executor = Failsafe.with(timeout); Middleware timeoutMiddleware = (request, next) -> executor.getStageAsync(() -> next.apply(request)); ProjectApiRoot apiRoot = ApiRootBuilder .of() .defaultClient( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), ServiceRegion.GCP_EUROPE_WEST1 ) .addMiddleware(timeoutMiddleware) .build("my-project-key"); ``` #### HTTP client timeout The underlying HTTP client can be configured with a fixed timeout: ```java title="Configure the HTTP client timeout" // OkHttp VrapHttpClient httpClient = new CtOkHttp4Client( builder -> builder .connectTimeout(Duration.ofSeconds(5)) .readTimeout(Duration.ofSeconds(30)) ); // Apache AsyncHTTP VrapHttpClient httpClient = new CtApacheHttpClient( builder -> builder.setResponseTimeout(Timeout.ofSeconds(30)) ); ``` #### Future timeout The third option is to use the timeout functionality of Java [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html): ```java title="Set a timeout on a CompletableFuture" apiRoot .shoppingLists() .get() .execute() .orTimeout(10, TimeUnit.SECONDS) .thenAccept(response -> { // handle response }); ``` ### HTTP protocol version OkHttp and Apache AsyncHTTP use HTTP/1.1 by default. To specify a different HTTP protocol version, configure the HTTP client package directly: ```java title="Enable HTTP/2" VrapHttpClient httpClient = new CtOkHttp4Client( builder -> builder.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) ); ProjectApiRoot apiRoot = ApiRootBuilder .of(httpClient) .defaultClient( ClientCredentials .of() .withClientId("your-client-id") .withClientSecret("your-client-secret") .build(), ServiceRegion.GCP_EUROPE_WEST1 ) .build("my-project-key"); ``` ## Subscriptions ### Deserializing delivery payloads The SDK provides [JsonUtils](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/io/vrap/rmf/base/client/utils/json/JsonUtils.html) which can deserialize [subscription](/api/projects/subscriptions.md) delivery payloads. Using the [DeliveryPayload](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/models/subscription/DeliveryPayload.html) class, it correctly deserializes the payload into one of: - [MessageDeliveryPayload](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/models/subscription/MessageDeliveryPayload.html) - [ResourceCreatedDeliveryPayload](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/models/subscription/ResourceCreatedDeliveryPayload.html) - [ResourceUpdatedDeliveryPayload](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/models/subscription/ResourceUpdatedDeliveryPayload.html) - [ResourceDeletedDeliveryPayload](https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/com/commercetools/api/models/subscription/ResourceDeletedDeliveryPayload.html) ```java title="Deserialize a subscription delivery payload" String deliveryPayloadJson = "..."; // raw JSON from subscription delivery DeliveryPayload payload = JsonUtils .fromJsonString(deliveryPayloadJson, DeliveryPayload.class); if (payload instanceof MessageDeliveryPayload) { MessageDeliveryPayload messagePayload = (MessageDeliveryPayload) payload; // access message-specific fields } ``` `MessageDeliveryPayload` also includes a helper method to cast to the inner message delivery and return the wrapped message: ```java title="Access the inner message from a delivery payload" MessageDeliveryPayload messagePayload = (MessageDeliveryPayload) payload; Message message = messagePayload.getMessage(); ``` ## Optimize package size When developing small function applications in size-constrained environments such as AWS Lambda, the full `commercetools-sdk-java-api` package may be larger than needed. The SDK offers packages split by domain. You must explicitly declare the domain packages you intend to use. | Artifact ID | Description | | --- | --- | | `commercetools-sdk-java-api-base` | Base SDK client infrastructure | | `commercetools-sdk-java-api-models-base` | Base model classes | | `commercetools-sdk-java-api-models-{domain}` | Models for a specific domain (for example, `products`, `orders`) | | `commercetools-sdk-java-api-predicates` | Query Predicate builders | An [example using split packages](https://github.com/commercetools/commercetools-sdk-java-v2/tree/main/examples/split-packages) is available on GitHub. ## Related pages - [Area overview page with navigation](/dev-tooling.md) - [Previous page: Middleware](/dev-tooling/java-sdk-middleware.md) - [Next page: Best practices](/dev-tooling/java-sdk-best-practices.md)