How to upgrade the Java SDK from v2 to v3

This guide will help you navigate through changes included in version 3 of the Account Protect Java SDK for a successful upgrade.

Version 3 is generated from the Account Protect OpenAPI specification, which changes the client, event, and response types throughout the SDK.

🚧

Java 17 and Spring Boot 3 required

Version 3 imports jakarta.servlet.http.HttpServletRequest instead of javax.servlet.http.HttpServletRequest, so it requires Java 17+ and Spring Boot 3.x. If your application runs on Java 11 or Spring Boot 2.x, stay on the fraud-sdk-java 1.0.1 release until you can upgrade your runtime.

Overview of changes

  • Update client setup to a builder pattern with Client.builder(...), replacing the previous service and options classes.
  • Replace the single service with validate and collect methods with one dedicated class per operation, such as ValidateLogin and CollectCustom.
  • Replace the previous event builders with per-event payload builders.
  • Validate the payload at build time and fail on missing required fields, so the login status must now be set explicitly instead of being inferred from validate versus collect.
  • Replace the extracted request wrapper with a RequestMetadata object passed alongside the request.
  • Replace the response status field with checked exceptions: validate calls still fail open and allow, while collect and feedback calls now throw.
  • Remove the Response/ResponseAction helper methods isAllowed, isDenied, and getStatus.
  • Rename the address, title, and authentication model classes, and give each event its own user type instead of a shared one.
  • Replace array and date fields in the models with standard list and string representations.
  • Remove asynchronous validate and collect calls. Wrap the synchronous perform(...) call yourself if you need non-blocking behavior (see Step 6).
  • Remove the forwarded-header configuration option and its header parsing. Pass the forwarded host, protocol, and client IP yourself through RequestMetadata (see Step 7).
  • Remove support for non-servlet requests.
  • Add support for custom events with ValidateCustom and CollectCustom.
  • Add support for the feedback endpoint.
  • Add account type, account creation date, custom fields, and fail reason to the event payloads.

Migration steps

Step 1: Upgrade to the latest package version

<dependency>
  <groupId>co.datadome.fraud</groupId>
  <artifactId>fraud-sdk-java</artifactId>
  <version>3.0.0</version>
</dependency>

Step 2: Update the client instantiation

Replace DataDomeFraudService with Client, built with a builder:

import co.datadome.fraud.Client;

@Bean
public Client fraudClient() {
    return Client.builder("your-api-key").build();
}
import co.datadome.fraud.DataDomeFraudService;

@Bean
public DataDomeFraudService dataDomeFraud(
        @Value("${datadome.fraud.api_key}") String datadomeFraudApiKey) {
    return new DataDomeFraudService(datadomeFraudApiKey);
}

Step 3: Update event submission and payload construction

Replace the single validate/collect service with one dedicated class per operation. The table below maps every event to its v3 operations:

Eventv3 validate operationv3 collect operation
LoginValidateLoginCollectLogin
RegistrationValidateRegistrationCollectRegistration
Account updateValidateAccountUpdateCollectAccountUpdate
Password updateValidatePasswordUpdateCollectPasswordUpdate
Custom eventValidateCustomCollectCustom

The example below migrates a login event. Registration, account update, password update, and custom events follow the same pattern.

LoginPayload.Builder builder = LoginPayload.builder()
        .account(login)
        .status(LoginPayload.Status.SUCCEEDED)
        .user(user)
        .authentication(authentication)
        .session(session);

try {
    ResponseLogin validate = new ValidateLogin(builder).perform(client, request, null);
    if (validate.getAction() == ResponseAction.ALLOW) {
        return ResponseEntity.ok().build();
    }
    return ResponseEntity.status(403).body("denied by Account Protect API");
} catch (ApiException e) {
    logger.severe("error during validation: " + e.getMessage());
    return ResponseEntity.status(403).body("denied by Account Protect API");
}
LoginEvent loginEvent = LoginEvent.newBuilder()
        .account(login)
        .user(co.datadome.fraud.model.User.newBuilder().id("user_id").build())
        .authentication(Authentication.newBuilder()
                .type(AuthenticationType.SOCIAL)
                .socialProvider(SocialProvider.GOOGLE)
                .mode(AuthenticationMode.PASSWORD)
                .build())
        .session(Session.newBuilder().id("session_id").build())
        .build();

DataDomeResponse validate = this.dataDomeFraudService.validate(request, loginEvent);
if (validate.getAction() == ResponseActionType.allow) {
    return ResponseEntity.ok().build();
}
return ResponseEntity.status(403).body("denied by Account Protect API");
🚧

The login status is now explicit

In v2, the status was inferred from calling validate versus collect. In v3, pass it explicitly with LoginPayload.Builder.status(...), and call ValidateLogin for a SUCCEEDED status or CollectLogin for a FAILED status.

Note also that perform(client, request, requestMetadata) takes the Builder itself, not a built payload.

For a failed login, CollectLogin throws instead of returning a status:

LoginPayload.Builder builder = LoginPayload.builder()
        .account(login)
        .status(LoginPayload.Status.FAILED);
try {
    new CollectLogin(builder).perform(client, request, null);
} catch (ApiException e) {
    logger.severe("error during collection: " + e.getMessage());
}
this.dataDomeFraudService.collect(request, new LoginEvent(login));

Step 4: Update your models

Replace the fluent newBuilder()...build() models with JavaBean setters:

UserAllOfAddress userAddress = new UserAllOfAddress();
userAddress.setName("Élysée Palace");
userAddress.setLine1("55 Rue du Faubourg Saint-Honoré");
userAddress.setLine2("2nd floor");
userAddress.setCity("Paris");
userAddress.setCountryCode("FR");
userAddress.setRegionCode("75");
userAddress.setZipCode("75008");
Address userAddress = Address.newBuilder()
        .name("Élysée Palace")
        .line1("55 Rue du Faubourg Saint-Honoré")
        .line2("2nd floor")
        .city("Paris")
        .country("France")
        .regionCode("75")
        .zipCode("75008")
        .build();

This table maps the renamed model classes:

v2v3
AddressUserAllOfAddress, with the country field removed. Keep countryCode.
UserLoginPayloadAllOfUser, RegistrationPayloadAllOfUser, AccountUpdatePayloadAllOfUser, PasswordUpdatePayloadAllOfUser, or CustomActionPayloadAnyOf1AllOfUser, depending on the event
AuthenticationLoginPayloadAllOfAuthentication, RegistrationPayloadAllOfAuthentication, or AccountUpdatePayloadAllOfAuthentication, depending on the event
DataDomeResponseResponse, or ResponseLogin for the login event

Replace array and date fields with List and String:

user.setPictureUrls(List.of("https://example.org/image1.png"));
user.setExternalUrls(List.of("https://example.org/external1"));
user.setCreatedAt(Instant.now().toString());
.pictureUrls(new String[]{"https://example.org/image1.png"})
.externalUrls(new String[]{"https://example.org/external1"})
.createdAt(Date.from(Instant.now()))

Step 5: Update your response and error handling

Replace the Response/ResponseAction helper methods with an explicit comparison to ResponseAction.ALLOW, and catch the new checked exceptions:

try {
    ResponseLogin validate = new ValidateLogin(builder).perform(client, request, null);
    if (validate.getAction() == ResponseAction.ALLOW) {
        // allowed
    }
} catch (ApiException e) {
    // handle the error
}
DataDomeResponse validate = this.dataDomeFraudService.validate(request, loginEvent);
if (validate.isAllowed()) {
    // allowed
}

ApiException is the base checked exception thrown by collect and feedback calls. The SDK also defines HttpException, RequestTimeoutException, and ValidationException for more specific error handling.

Step 6: Handle asynchronous calls yourself

Version 2 exposed validateAsync and collectAsync, which returned a CompletableFuture<DataDomeResponse>. Version 3 only exposes the synchronous perform(client, request, requestMetadata) method on each operation class: there's no asynchronous equivalent.

If your application relies on non-blocking calls, wrap perform(...) in your own CompletableFuture, using an executor of your choice instead of the default ForkJoinPool.commonPool() to avoid starving it with blocking network calls:

CompletableFuture<ResponseLogin> future = CompletableFuture.supplyAsync(() -> {
    try {
        return new ValidateLogin(builder).perform(client, request, null);
    } catch (ApiException e) {
        throw new CompletionException(e);
    }
}, executor);
CompletableFuture<DataDomeResponse> future =
        this.dataDomeFraudService.validateAsync(request, loginEvent);

Step 7: Pass forwarded-proxy headers through RequestMetadata

Version 2's useForwarded(true) option told the SDK to automatically override the extracted host, protocol, and forwarded client IP from the X-Forwarded-Host, X-Forwarded-Proto, and RFC 7239 Forwarded request headers, for applications running behind a reverse proxy or load balancer. Version 3 removes this option and its header parsing entirely.

If your application runs behind a reverse proxy or load balancer, extract these values yourself from the incoming request and set them explicitly on a RequestMetadata object, passed as the third argument to perform(...). Each non-null field on RequestMetadata overrides the corresponding value the SDK would otherwise read from the request:

RequestMetadata metadata = new RequestMetadata()
        .setHost(request.getHeader("x-forwarded-host"))
        .setProtocol(request.getHeader("x-forwarded-proto"))
        .setXForwardedForIp(request.getHeader("x-forwarded-for"));

ResponseLogin validate = new ValidateLogin(builder).perform(client, request, metadata);
DataDomeOptions options = DataDomeOptions.newBuilder().useForwarded(true).build();
DataDomeFraudService dataDomeFraudService = new DataDomeFraudService(datadomeFraudApiKey, options);

// The SDK automatically read X-Forwarded-Host, X-Forwarded-Proto, and the
// RFC 7239 Forwarded header to override host, protocol, and the forwarded IP.
DataDomeResponse validate = dataDomeFraudService.validate(request, loginEvent);
🚧

If your proxy sends the RFC 7239 Forwarded header

Version 2 also parsed the standard Forwarded header (for example, Forwarded: for=203.0.113.1;host=example.com;proto=https) as a fallback to the individual X-Forwarded-* headers. Version 3 doesn't parse it. If your proxy only sends Forwarded, parse its for, host, and proto key-value pairs yourself and set them on RequestMetadata with setXForwardedForIp, setHost, and setProtocol.

Reference

Enum relocations

v2v3
Title.MRUserTitle.MR
AuthenticationMode.PASSWORDAuthentication.Mode.PASSWORD
SocialProvider.GOOGLEAuthentication.SocialProvider.GOOGLE
AuthenticationType.SOCIALAuthentication.Type.SOCIAL
ResponseActionType.allowResponseAction.ALLOW
PasswordUpdateStatus.SUCCEEDEDPasswordUpdatePayload.Status.SUCCEEDED
PasswordUpdateReason.USER_UPDATEPasswordUpdatePayload.Reason.USER_UPDATE

Removed configuration

v2v3
datadome.fraud.api_key property, injected with @ValuePass the API key directly to Client.builder(key)
validateAsync, collectAsyncRemoved. Wrap perform(...) in your own CompletableFuture (see Step 6).
DataDomeOptions.useForwarded(true)Removed. Set RequestMetadata.setHost/setProtocol/setXForwardedForIp yourself (see Step 7).

Did this page help you?