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 requiredVersion 3 imports
jakarta.servlet.http.HttpServletRequestinstead ofjavax.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 thefraud-sdk-java1.0.1release 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
ValidateLoginandCollectCustom. - 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
RequestMetadataobject 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/ResponseActionhelper methodsisAllowed,isDenied, andgetStatus. - 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
ValidateCustomandCollectCustom. - 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:
| Event | v3 validate operation | v3 collect operation |
|---|---|---|
| Login | ValidateLogin | CollectLogin |
| Registration | ValidateRegistration | CollectRegistration |
| Account update | ValidateAccountUpdate | CollectAccountUpdate |
| Password update | ValidatePasswordUpdate | CollectPasswordUpdate |
| Custom event | ValidateCustom | CollectCustom |
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 explicitIn v2, the status was inferred from calling
validateversuscollect. In v3, pass it explicitly withLoginPayload.Builder.status(...), and callValidateLoginfor aSUCCEEDEDstatus orCollectLoginfor aFAILEDstatus.Note also that
perform(client, request, requestMetadata)takes theBuilderitself, 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:
| v2 | v3 |
|---|---|
Address | UserAllOfAddress, with the country field removed. Keep countryCode. |
User | LoginPayloadAllOfUser, RegistrationPayloadAllOfUser, AccountUpdatePayloadAllOfUser, PasswordUpdatePayloadAllOfUser, or CustomActionPayloadAnyOf1AllOfUser, depending on the event |
Authentication | LoginPayloadAllOfAuthentication, RegistrationPayloadAllOfAuthentication, or AccountUpdatePayloadAllOfAuthentication, depending on the event |
DataDomeResponse | Response, 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 7239ForwardedheaderVersion 2 also parsed the standard
Forwardedheader (for example,Forwarded: for=203.0.113.1;host=example.com;proto=https) as a fallback to the individualX-Forwarded-*headers. Version 3 doesn't parse it. If your proxy only sendsForwarded, parse itsfor,host, andprotokey-value pairs yourself and set them onRequestMetadatawithsetXForwardedForIp,setHost, andsetProtocol.
Reference
Enum relocations
| v2 | v3 |
|---|---|
Title.MR | UserTitle.MR |
AuthenticationMode.PASSWORD | Authentication.Mode.PASSWORD |
SocialProvider.GOOGLE | Authentication.SocialProvider.GOOGLE |
AuthenticationType.SOCIAL | Authentication.Type.SOCIAL |
ResponseActionType.allow | ResponseAction.ALLOW |
PasswordUpdateStatus.SUCCEEDED | PasswordUpdatePayload.Status.SUCCEEDED |
PasswordUpdateReason.USER_UPDATE | PasswordUpdatePayload.Reason.USER_UPDATE |
Removed configuration
| v2 | v3 |
|---|---|
datadome.fraud.api_key property, injected with @Value | Pass the API key directly to Client.builder(key) |
validateAsync, collectAsync | Removed. Wrap perform(...) in your own CompletableFuture (see Step 6). |
DataDomeOptions.useForwarded(true) | Removed. Set RequestMetadata.setHost/setProtocol/setXForwardedForIp yourself (see Step 7). |
Updated 8 days ago

