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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## Unreleased

### Changed

- Use Jackson 3 for JSON serialization. Jackson annotations remain unchanged.
- Remove the Jackson 2 mapper and `TypeReference` APIs deprecated in the bridge release.
- Keep Jackson core and databind as runtime dependencies, not compile dependencies.
- Remove the unused `jackson-databind-nullable` dependency.

### Jackson 3 migration

This change is for the next major release. Publish the Jackson 2 bridge release
before removing its deprecated APIs. The 0.x line remains on Jackson 2 for security fixes.

Applications that use the default serializer do not need source changes.
For custom serialization, implement `dev.openfga.sdk.api.client.JsonSerializer`
and pass it to `ApiClient(HttpClient.Builder, JsonSerializer)` or
`ApiClient.setJsonSerializer(...)`. Use `JsonSerializer.createDefault()` to restore
the SDK serializer. This release removes the `ObjectMapper` constructor,
accessors, and protected `createDefaultObjectMapper()` method.

Replace Jackson `TypeReference` arguments with
`dev.openfga.sdk.api.client.SdkTypeToken`, including in `BaseStreamingApi`
subclasses. Use its `jsonSerializer` and `streamResultType` fields instead of the
removed `objectMapper` and `streamResultTypeRef` fields. Serialization failures
continue to use `SdkSerializationException`.

Applications that use Jackson directly must declare their own Jackson core and
databind dependencies. For Jackson 3, use the `tools.jackson.core` coordinates.
The `com.fasterxml.jackson.annotation` package remains available through the SDK.

## [0.10.0](https://github.com/openfga/java-sdk/compare/v0.9.11...v0.10.0) (2026-09-02)

> [!WARNING]
Expand Down
20 changes: 10 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jacocoTestReport {
}

ext {
jackson_version = "2.22.2"
jackson_version = "3.2.2"
}

configurations.testRuntimeClasspath {
Expand All @@ -70,12 +70,10 @@ dependencies {
api "com.google.code.findbugs:jsr305:3.0.2"

// ---- Jackson ----
api platform("com.fasterxml.jackson:jackson-bom:$jackson_version")
api "com.fasterxml.jackson.core:jackson-core"
api "com.fasterxml.jackson.core:jackson-annotations"
api "com.fasterxml.jackson.core:jackson-databind"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
implementation "org.openapitools:jackson-databind-nullable:0.2.11"
implementation platform("tools.jackson:jackson-bom:$jackson_version")
api "com.fasterxml.jackson.core:jackson-annotations:2.22"
implementation "tools.jackson.core:jackson-core"
implementation "tools.jackson.core:jackson-databind"

// ---- OpenTelemetry ----
api platform("io.opentelemetry:opentelemetry-bom:1.65.0")
Expand All @@ -91,6 +89,8 @@ testing {
implementation 'org.mockito:mockito-core:5.23.0'
implementation 'org.junit.jupiter:junit-jupiter:5.14.4'
implementation 'org.wiremock:wiremock:3.13.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.22.2'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.22.2'

runtimeOnly 'org.junit.platform:junit-platform-launcher'

Expand All @@ -117,9 +117,9 @@ testing {

dependencies {
// --- Jackson ---
implementation platform("com.fasterxml.jackson:jackson-bom:$jackson_version")
implementation "com.fasterxml.jackson.core:jackson-core"
implementation "com.fasterxml.jackson.core:jackson-databind"
implementation platform("tools.jackson:jackson-bom:$jackson_version")
implementation "tools.jackson.core:jackson-core"
implementation "tools.jackson.core:jackson-databind"

implementation "org.testcontainers:testcontainers-junit-jupiter:2.0.5"
implementation "org.testcontainers:testcontainers-openfga:2.0.5"
Expand Down
16 changes: 8 additions & 8 deletions docs/ApiExecutor.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ StreamingApiExecutor<MyResponse> executor = client.streamingApiExecutor(MyRespon

**Access — escape hatch (when T is itself generic):**
```java
TypeReference<StreamResult<MyResponse>> typeRef = new TypeReference<StreamResult<MyResponse>>() {};
StreamingApiExecutor<MyResponse> executor = client.streamingApiExecutor(typeRef);
SdkTypeToken<StreamResult<MyResponse>> type = new SdkTypeToken<StreamResult<MyResponse>>() {};
StreamingApiExecutor<MyResponse> executor = client.streamingApiExecutor(type);
```

**Methods:**
Expand Down Expand Up @@ -147,20 +147,20 @@ client.streamingApiExecutor(StreamedListObjectsResponse.class)
.thenRun(() -> System.out.println("Received " + objects.size() + " objects"));
```

### Streaming endpoint with TypeReference (escape hatch for generic response types)
### Streaming endpoint with a generic response type

Use `TypeReference` only when the response type `T` is itself generic. For all concrete
types — which covers the vast majority of endpoints — use `streamingApiExecutor(MyResponse.class)` instead.
Use `SdkTypeToken` when the response type `T` is generic. For concrete response
types, use `streamingApiExecutor(MyResponse.class)` instead.

```java
// Hypothetical endpoint whose response wraps a generic Page<Item>
TypeReference<StreamResult<Page<Item>>> typeRef = new TypeReference<StreamResult<Page<Item>>>() {};
SdkTypeToken<StreamResult<Page<Item>>> type = new SdkTypeToken<StreamResult<Page<Item>>>() {};
Comment thread
Adrastopoulos marked this conversation as resolved.

ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder(HttpMethod.POST, "/stores/{store_id}/streamed-paged-items")
.body(requestBody)
.build();

client.streamingApiExecutor(typeRef)
client.streamingApiExecutor(type)
.stream(request, page -> page.getItems().forEach(System.out::println))
.thenRun(() -> System.out.println("Done"));
```
Expand Down Expand Up @@ -214,7 +214,7 @@ ApiExecutorRequestBuilder.builder(HttpMethod.POST, "/stores/{store_id}/settings"
- Path/query parameters are URL-encoded automatically
- Authentication tokens injected from client config
- `{store_id}` auto-replaced if not provided via `.pathParam()`
- For `StreamingApiExecutor`, pass the response class directly (`MyResponse.class`). The SDK builds the required Jackson type internally. Use the `TypeReference` overload only when `T` is itself a generic type.
- For `StreamingApiExecutor`, pass the response class directly (`MyResponse.class`). Use the `SdkTypeToken` overload when `T` is a generic type.

## Migration to Typed Methods

Expand Down
6 changes: 3 additions & 3 deletions examples/api-executor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ client.streamingApiExecutor(MyStreamedResponse.class)
.thenRun(() -> System.out.println("Stream complete"));
```

If your response type is itself generic, use the `TypeReference` overload:
If your response type is generic, use the `SdkTypeToken` overload:
```java
TypeReference<StreamResult<MyStreamedResponse>> typeRef = new TypeReference<StreamResult<MyStreamedResponse>>() {};
client.streamingApiExecutor(typeRef).stream(request, consumer);
SdkTypeToken<StreamResult<MyStreamedResponse>> type = new SdkTypeToken<StreamResult<MyStreamedResponse>>() {};
client.streamingApiExecutor(type).stream(request, consumer);
```

### SDK Features Applied
Expand Down
14 changes: 7 additions & 7 deletions examples/api-executor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repositories {
}

ext {
jacksonVersion = "2.18.2"
jacksonVersion = "3.2.2"
}

dependencies {
Expand All @@ -29,14 +29,14 @@ dependencies {
implementation("dev.openfga:openfga-language:v0.2.0-beta.1")

// Serialization
implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
implementation("org.openapitools:jackson-databind-nullable:0.2.7")
implementation(platform("tools.jackson:jackson-bom:$jacksonVersion"))
implementation("tools.jackson.core:jackson-databind")

// openfga-language still requires Jackson 2.
implementation(platform("com.fasterxml.jackson:jackson-bom:2.22.2"))

// OpenTelemetry (required by SDK)
implementation platform("io.opentelemetry:opentelemetry-bom:1.54.1")
implementation platform("io.opentelemetry:opentelemetry-bom:1.65.0")
implementation "io.opentelemetry:opentelemetry-api"

// JSR305 (required by SDK)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package dev.openfga.sdk.example;

import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder;
import dev.openfga.sdk.api.client.HttpMethod;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
import dev.openfga.sdk.api.model.CreateStoreResponse;
import dev.openfga.sdk.api.model.ListStoresResponse;
Expand Down Expand Up @@ -64,7 +64,8 @@ public static void main(String[] args) throws Exception {
private static String listStoresExample(OpenFgaClient fgaClient) {
try {
// Build the raw request for GET /stores
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder(HttpMethod.GET, "/stores").build();
ApiExecutorRequestBuilder request =
ApiExecutorRequestBuilder.builder(HttpMethod.GET, "/stores").build();

// Execute with typed response
var response = fgaClient
Expand Down Expand Up @@ -101,13 +102,15 @@ private static String listStoresExample(OpenFgaClient fgaClient) {
* Helper method to create a store for examples.
*/
private static String createStoreForExamples(OpenFgaClient fgaClient) throws Exception {
String storeName = "api-executor-example-" + UUID.randomUUID().toString().substring(0, 8);
String storeName =
"api-executor-example-" + UUID.randomUUID().toString().substring(0, 8);
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder(HttpMethod.POST, "/stores")
.body(Map.of("name", storeName))
.build();

// Use typed response instead of manual JSON parsing
var response = fgaClient.apiExecutor().send(request, CreateStoreResponse.class).get();
var response =
fgaClient.apiExecutor().send(request, CreateStoreResponse.class).get();
System.out.println(" Created store: " + storeName);
return response.getData().getId();
}
Expand Down Expand Up @@ -148,7 +151,8 @@ private static void listStoresWithPaginationExample(OpenFgaClient fgaClient) {
.get();

System.out.println("✓ Status: " + response.getStatusCode());
System.out.println("✓ Stores returned: " + response.getData().getStores().size());
System.out.println(
"✓ Stores returned: " + response.getData().getStores().size());
if (response.getData().getContinuationToken() != null) {
String token = response.getData().getContinuationToken();
String tokenPreview = token.length() > 20 ? token.substring(0, 20) + "..." : token;
Expand All @@ -167,7 +171,8 @@ private static void listStoresWithPaginationExample(OpenFgaClient fgaClient) {
*/
private static void createStoreWithHeadersExample(OpenFgaClient fgaClient) {
try {
String storeName = "raw-api-custom-headers-" + UUID.randomUUID().toString().substring(0, 8);
String storeName =
"raw-api-custom-headers-" + UUID.randomUUID().toString().substring(0, 8);
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder(HttpMethod.POST, "/stores")
.header("X-Example-Header", "custom-value")
.header("X-Request-ID", "req-" + UUID.randomUUID())
Expand Down Expand Up @@ -209,4 +214,3 @@ private static void errorHandlingExample(OpenFgaClient fgaClient) {
}
}
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package dev.openfga.sdk.example;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.language.DslToJsonTransformer;
import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder;
import dev.openfga.sdk.api.client.HttpMethod;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.SdkTypeToken;
import dev.openfga.sdk.api.client.model.ClientTupleKey;
import dev.openfga.sdk.api.client.model.ClientWriteRequest;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
Expand All @@ -19,6 +18,7 @@
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import tools.jackson.databind.json.JsonMapper;

/**
* Example demonstrating {@link dev.openfga.sdk.api.client.StreamingApiExecutor} usage.
Expand All @@ -37,7 +37,7 @@
* <li>Calls {@code /stores/{store_id}/streamed-list-objects} via
* {@code client.streamingApiExecutor(StreamedListObjectsResponse.class).stream(request, consumer)}
* to stream all 200 objects back — the preferred API for concrete response types.</li>
* <li>Repeats the same call using the {@code TypeReference} overload — for cases where
* <li>Repeats the same call using the {@code SdkTypeToken} overload — for cases where
* the response type is itself generic.</li>
* <li>Cleans up the store.</li>
* </ol>
Expand Down Expand Up @@ -73,8 +73,8 @@ public static void main(String[] args) {
System.err.println("Is OpenFGA server running? Check " + ENV_API_URL
+ " environment variable or default " + DEFAULT_API_URL);
} else {
System.err.println("An error occurred. [" + ex.getClass().getSimpleName() + ": " + ex.getMessage()
+ "]");
System.err.println(
"An error occurred. [" + ex.getClass().getSimpleName() + ": " + ex.getMessage() + "]");
}
System.exit(1);
}
Expand Down Expand Up @@ -132,9 +132,7 @@ public void run() throws Exception {
tuples.add(new ClientTupleKey()
.user(USER_ANNE)
.relation(RELATION_VIEWER)
._object(DOCUMENT_TYPE
+ ":"
+ (VIEWER_DOCUMENT_OFFSET + batch * WRITE_BATCH_SIZE + i)));
._object(DOCUMENT_TYPE + ":" + (VIEWER_DOCUMENT_OFFSET + batch * WRITE_BATCH_SIZE + i)));
}
fga.write(new ClientWriteRequest().writes(tuples)).get();
totalWritten += tuples.size();
Expand Down Expand Up @@ -164,8 +162,7 @@ public void run() throws Exception {
AtomicInteger count = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);

fga.streamingApiExecutor(StreamedListObjectsResponse.class)
.stream(
fga.streamingApiExecutor(StreamedListObjectsResponse.class).stream(
request,
response -> {
int n = count.incrementAndGet();
Expand All @@ -185,21 +182,20 @@ public void run() throws Exception {
}

// ------------------------------------------------------------------ //
// 3. Same call using the TypeReference overload //
// 3. Same call using the SdkTypeToken overload //
// Use this when the response type T is itself generic. //
// For concrete types like StreamedListObjectsResponse, the //
// Class<T> overload above is simpler. //
// ------------------------------------------------------------------ //

System.out.println("\nRepeating via TypeReference overload...");
System.out.println("\nRepeating via SdkTypeToken overload...");

TypeReference<StreamResult<StreamedListObjectsResponse>> typeRef =
new TypeReference<StreamResult<StreamedListObjectsResponse>>() {};
SdkTypeToken<StreamResult<StreamedListObjectsResponse>> typeRef =
new SdkTypeToken<StreamResult<StreamedListObjectsResponse>>() {};

AtomicInteger typeRefCount = new AtomicInteger(0);

fga.streamingApiExecutor(typeRef)
.stream(
fga.streamingApiExecutor(typeRef).stream(
request,
response -> {
int n = typeRefCount.incrementAndGet();
Expand All @@ -210,7 +206,7 @@ public void run() throws Exception {
err -> System.err.println(" Stream error: " + err.getMessage()))
.get();

System.out.println("\n✓ Streamed " + typeRefCount.get() + " objects via TypeReference overload");
System.out.println("\n✓ Streamed " + typeRefCount.get() + " objects via SdkTypeToken overload");

// ------------------------------------------------------------------ //
// 4. Clean up //
Expand Down Expand Up @@ -247,8 +243,8 @@ private WriteAuthorizationModelRequest createAuthorizationModel() {

try {
var jsonModel = new DslToJsonTransformer().transform(dslModel);
var mapper = new ObjectMapper();
mapper.findAndRegisterModules();
var mapper = JsonMapper.builderWithJackson2Defaults().build();

var authModel = mapper.readValue(jsonModel, AuthorizationModel.class);
return new WriteAuthorizationModelRequest()
.typeDefinitions(authModel.getTypeDefinitions())
Expand All @@ -259,4 +255,3 @@ private WriteAuthorizationModelRequest createAuthorizationModel() {
}
}
}

9 changes: 3 additions & 6 deletions examples/basic-examples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,15 @@ repositories {
}

ext {
jacksonVersion = "2.22.0"
jacksonVersion = "3.2.2"
}

dependencies {
implementation("dev.openfga:openfga-sdk:0.10.0") // x-release-please-version

// Serialization
implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
implementation("org.openapitools:jackson-databind-nullable:0.2.11")
implementation(platform("tools.jackson:jackson-bom:$jacksonVersion"))
implementation("tools.jackson.core:jackson-databind")

// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.openfga.sdk.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.client.ClientAssertion;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.*;
Expand All @@ -11,6 +10,7 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import tools.jackson.databind.json.JsonMapper;

class Example1 {
public void run(String apiUrl) throws Exception {
Expand Down Expand Up @@ -73,7 +73,7 @@ public void run(String apiUrl) throws Exception {
System.out.println("Latest Authorization Model not found");
}

var mapper = new ObjectMapper().findAndRegisterModules();
var mapper = JsonMapper.builderWithJackson2Defaults().build();

// WriteAuthorizationModel
var authModelJson = loadResource("example1-auth-model.json");
Expand Down
Loading
Loading