diff --git a/CHANGELOG.md b/CHANGELOG.md index 84415cbc..401369c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/build.gradle b/build.gradle index f3cbb61d..347dc77e 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ jacocoTestReport { } ext { - jackson_version = "2.22.2" + jackson_version = "3.2.2" } configurations.testRuntimeClasspath { @@ -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") @@ -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' @@ -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" diff --git a/docs/ApiExecutor.md b/docs/ApiExecutor.md index b9e8866e..f4bdd483 100644 --- a/docs/ApiExecutor.md +++ b/docs/ApiExecutor.md @@ -81,8 +81,8 @@ StreamingApiExecutor executor = client.streamingApiExecutor(MyRespon **Access — escape hatch (when T is itself generic):** ```java -TypeReference> typeRef = new TypeReference>() {}; -StreamingApiExecutor executor = client.streamingApiExecutor(typeRef); +SdkTypeToken> type = new SdkTypeToken>() {}; +StreamingApiExecutor executor = client.streamingApiExecutor(type); ``` **Methods:** @@ -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 -TypeReference>> typeRef = new TypeReference>>() {}; +SdkTypeToken>> type = new SdkTypeToken>>() {}; 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")); ``` @@ -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 diff --git a/examples/api-executor/README.md b/examples/api-executor/README.md index 82123986..70315332 100644 --- a/examples/api-executor/README.md +++ b/examples/api-executor/README.md @@ -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> typeRef = new TypeReference>() {}; -client.streamingApiExecutor(typeRef).stream(request, consumer); +SdkTypeToken> type = new SdkTypeToken>() {}; +client.streamingApiExecutor(type).stream(request, consumer); ``` ### SDK Features Applied diff --git a/examples/api-executor/build.gradle b/examples/api-executor/build.gradle index 475d2de3..d3c687ce 100644 --- a/examples/api-executor/build.gradle +++ b/examples/api-executor/build.gradle @@ -18,7 +18,7 @@ repositories { } ext { - jacksonVersion = "2.18.2" + jacksonVersion = "3.2.2" } dependencies { @@ -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) diff --git a/examples/api-executor/src/main/java/dev/openfga/sdk/example/ApiExecutorExample.java b/examples/api-executor/src/main/java/dev/openfga/sdk/example/ApiExecutorExample.java index b19426f8..b8bb2e5d 100644 --- a/examples/api-executor/src/main/java/dev/openfga/sdk/example/ApiExecutorExample.java +++ b/examples/api-executor/src/main/java/dev/openfga/sdk/example/ApiExecutorExample.java @@ -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; @@ -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 @@ -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(); } @@ -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; @@ -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()) @@ -209,4 +214,3 @@ private static void errorHandlingExample(OpenFgaClient fgaClient) { } } } - diff --git a/examples/api-executor/src/main/java/dev/openfga/sdk/example/StreamingApiExecutorExample.java b/examples/api-executor/src/main/java/dev/openfga/sdk/example/StreamingApiExecutorExample.java index 78677f28..9ac8728e 100644 --- a/examples/api-executor/src/main/java/dev/openfga/sdk/example/StreamingApiExecutorExample.java +++ b/examples/api-executor/src/main/java/dev/openfga/sdk/example/StreamingApiExecutorExample.java @@ -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; @@ -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. @@ -37,7 +37,7 @@ *
  • 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.
  • - *
  • Repeats the same call using the {@code TypeReference} overload — for cases where + *
  • Repeats the same call using the {@code SdkTypeToken} overload — for cases where * the response type is itself generic.
  • *
  • Cleans up the store.
  • * @@ -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); } @@ -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(); @@ -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(); @@ -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 overload above is simpler. // // ------------------------------------------------------------------ // - System.out.println("\nRepeating via TypeReference overload..."); + System.out.println("\nRepeating via SdkTypeToken overload..."); - TypeReference> typeRef = - new TypeReference>() {}; + SdkTypeToken> typeRef = + new SdkTypeToken>() {}; AtomicInteger typeRefCount = new AtomicInteger(0); - fga.streamingApiExecutor(typeRef) - .stream( + fga.streamingApiExecutor(typeRef).stream( request, response -> { int n = typeRefCount.incrementAndGet(); @@ -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 // @@ -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()) @@ -259,4 +255,3 @@ private WriteAuthorizationModelRequest createAuthorizationModel() { } } } - diff --git a/examples/basic-examples/build.gradle b/examples/basic-examples/build.gradle index 9de5edc6..68a08c89 100644 --- a/examples/basic-examples/build.gradle +++ b/examples/basic-examples/build.gradle @@ -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" diff --git a/examples/basic-examples/src/main/java/dev/openfga/sdk/example/Example1.java b/examples/basic-examples/src/main/java/dev/openfga/sdk/example/Example1.java index 8a2e27f6..f9e82029 100644 --- a/examples/basic-examples/src/main/java/dev/openfga/sdk/example/Example1.java +++ b/examples/basic-examples/src/main/java/dev/openfga/sdk/example/Example1.java @@ -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.*; @@ -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 { @@ -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"); diff --git a/examples/basic-examples/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt b/examples/basic-examples/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt index aeedb6bc..38818a50 100644 --- a/examples/basic-examples/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt +++ b/examples/basic-examples/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt @@ -1,6 +1,6 @@ package dev.openfga.sdk.example -import com.fasterxml.jackson.databind.ObjectMapper +import tools.jackson.databind.json.JsonMapper import dev.openfga.sdk.api.client.ClientAssertion import dev.openfga.sdk.api.client.OpenFgaClient import dev.openfga.sdk.api.client.model.* @@ -77,7 +77,7 @@ internal class KotlinExample1 { } catch (e: Exception) { println("Latest Authorization Model not found") } - val mapper = ObjectMapper().findAndRegisterModules() + val mapper = JsonMapper.builderWithJackson2Defaults().build() // WriteAuthorizationModel val authModelJson = loadResource("example1-auth-model.json") diff --git a/examples/streamed-list-objects/build.gradle b/examples/streamed-list-objects/build.gradle index 843262d5..40d404e7 100644 --- a/examples/streamed-list-objects/build.gradle +++ b/examples/streamed-list-objects/build.gradle @@ -12,7 +12,7 @@ repositories { } ext { - jacksonVersion = "2.18.2" + jacksonVersion = "3.2.2" } dependencies { @@ -23,14 +23,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) diff --git a/examples/streamed-list-objects/src/main/java/dev/openfga/sdk/example/StreamedListObjectsExample.java b/examples/streamed-list-objects/src/main/java/dev/openfga/sdk/example/StreamedListObjectsExample.java index 6b07ed62..168720a6 100644 --- a/examples/streamed-list-objects/src/main/java/dev/openfga/sdk/example/StreamedListObjectsExample.java +++ b/examples/streamed-list-objects/src/main/java/dev/openfga/sdk/example/StreamedListObjectsExample.java @@ -1,6 +1,5 @@ package dev.openfga.sdk.example; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.language.DslToJsonTransformer; import dev.openfga.sdk.api.client.OpenFgaClient; import dev.openfga.sdk.api.client.model.ClientListObjectsRequest; @@ -16,6 +15,7 @@ import dev.openfga.sdk.errors.FgaInvalidParameterException; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; +import tools.jackson.databind.json.JsonMapper; public class StreamedListObjectsExample { // Configuration constants @@ -72,8 +72,8 @@ public void run() throws Exception { var client = new OpenFgaClient(configuration); System.out.println("Creating temporary store"); - var store = client.createStore(new CreateStoreRequest().name(STORE_NAME)) - .get(); + var store = + client.createStore(new CreateStoreRequest().name(STORE_NAME)).get(); var clientWithStore = new OpenFgaClient( new ClientConfiguration().apiUrl(apiUrl).storeId(store.getId()).credentials(new Credentials())); @@ -89,8 +89,8 @@ public void run() throws Exception { .authorizationModelId(authModel.getAuthorizationModelId()) .credentials(new Credentials())); - System.out.println("Writing tuples (" + TOTAL_OWNER_DOCUMENTS + " as owner, " + TOTAL_VIEWER_DOCUMENTS - + " as viewer)"); + System.out.println( + "Writing tuples (" + TOTAL_OWNER_DOCUMENTS + " as owner, " + TOTAL_VIEWER_DOCUMENTS + " as viewer)"); int totalWritten = 0; @@ -156,9 +156,9 @@ private WriteAuthorizationModelRequest createAuthorizationModel() { """ model schema 1.1 - + type %s - + type %s relations define %s: [%s] @@ -178,8 +178,7 @@ private WriteAuthorizationModelRequest createAuthorizationModel() { try { // Transform DSL to JSON and parse into AuthorizationModel var jsonModel = new DslToJsonTransformer().transform(dslModel); - var mapper = new ObjectMapper(); - mapper.findAndRegisterModules(); + var mapper = JsonMapper.builderWithJackson2Defaults().build(); var authModel = mapper.readValue(jsonModel, AuthorizationModel.class); diff --git a/src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java b/src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java index e0bc4f86..5525de51 100644 --- a/src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java +++ b/src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java @@ -14,8 +14,6 @@ import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.client.ApiClient; import dev.openfga.sdk.api.client.JsonSerializer; import dev.openfga.sdk.api.client.SdkTypeToken; @@ -24,7 +22,6 @@ import dev.openfga.sdk.api.model.StreamResult; import dev.openfga.sdk.errors.ApiException; import dev.openfga.sdk.errors.FgaInvalidParameterException; -import java.lang.reflect.Type; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.concurrent.CompletableFuture; @@ -44,17 +41,6 @@ public abstract class BaseStreamingApi { protected final JsonSerializer jsonSerializer; protected final SdkTypeToken> streamResultType; - /** - * Jackson mapper, or {@code null} when the serializer does not use Jackson 2. - * @deprecated Use {@link #jsonSerializer}. - */ - @Deprecated(since = "0.11.0") - protected final ObjectMapper objectMapper; - - /** @deprecated Use {@link #streamResultType}. */ - @Deprecated(since = "0.11.0") - protected final TypeReference> streamResultTypeRef; - /** * Constructor for BaseStreamingApi * @@ -68,43 +54,6 @@ protected BaseStreamingApi( this.apiClient = apiClient; this.jsonSerializer = apiClient.getJsonSerializer(); this.streamResultType = streamResultType; - ObjectMapper mapper; - try { - mapper = apiClient.getObjectMapper(); - } catch (UnsupportedOperationException ignored) { - mapper = null; - } - this.objectMapper = mapper; - this.streamResultTypeRef = new TypeReference>() { - @Override - public Type getType() { - return streamResultType.getType(); - } - }; - } - - /** - * Creates a streaming API with a Jackson type reference. - * - * @param configuration The API configuration - * @param apiClient The API client for making HTTP requests - * @param streamResultTypeRef Type reference for deserializing the stream result - * @deprecated Use {@link #BaseStreamingApi(Configuration, ApiClient, SdkTypeToken)}. - */ - @Deprecated(since = "0.11.0") - protected BaseStreamingApi( - Configuration configuration, ApiClient apiClient, TypeReference> streamResultTypeRef) { - this.configuration = configuration; - this.apiClient = apiClient; - this.objectMapper = apiClient.getObjectMapper(); - this.streamResultTypeRef = streamResultTypeRef; - this.jsonSerializer = apiClient.getJsonSerializer(); - this.streamResultType = new SdkTypeToken>() { - @Override - public Type getType() { - return streamResultTypeRef.getType(); - } - }; } /** diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java index 4c247455..d46eaab5 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java @@ -2,7 +2,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.auth.OAuth2Client; import dev.openfga.sdk.api.configuration.ClientCredentials; import dev.openfga.sdk.api.configuration.Configuration; @@ -55,7 +54,7 @@ public class ApiClient { */ public ApiClient() { this.builder = createDefaultHttpClientBuilder(); - this.jsonSerializer = new Jackson2JsonSerializer(createDefaultObjectMapper()); + this.jsonSerializer = JsonSerializer.createDefault(); this.client = this.builder.build(); interceptor = null; responseInterceptor = null; @@ -73,29 +72,13 @@ public ApiClient() { */ public ApiClient(HttpClient.Builder builder) { this.builder = builder; - this.jsonSerializer = new Jackson2JsonSerializer(createDefaultObjectMapper()); + this.jsonSerializer = JsonSerializer.createDefault(); this.client = this.builder.build(); interceptor = null; responseInterceptor = null; asyncResponseInterceptor = null; } - /** - * Create an instance of ApiClient. - *

    - * In other contexts, note that any settings in a {@link Configuration} - * will take precedence over equivalent settings in the - * {@link HttpClient.Builder} here. - * - * @param builder Http client builder. - * @param mapper Object mapper. - * @deprecated Use {@link #ApiClient(HttpClient.Builder, JsonSerializer)}. - */ - @Deprecated(since = "0.11.0") - public ApiClient(HttpClient.Builder builder, ObjectMapper mapper) { - this(builder, new Jackson2JsonSerializer(mapper)); - } - /** * Create an instance of ApiClient. * @@ -175,18 +158,6 @@ public static String urlEncode(String s) { return URLEncoder.encode(s, UTF_8).replaceAll("\\+", "%20"); } - /** - * Create the default Jackson 2 mapper used by constructors without an explicit serializer. - * - * @return The default object mapper. - * @deprecated Supply a {@link JsonSerializer} through - * {@link #ApiClient(HttpClient.Builder, JsonSerializer)} or {@link #setJsonSerializer(JsonSerializer)}. - */ - @Deprecated(since = "0.11.0") - protected ObjectMapper createDefaultObjectMapper() { - return Jackson2JsonSerializer.createDefaultObjectMapper(); - } - protected String getDefaultBaseUri() { return "http://localhost"; } @@ -234,33 +205,6 @@ public HttpClient.Builder getHttpClientBuilder() { return builder; } - /** - * Set a custom {@link ObjectMapper} for request and response bodies. - * - * @param mapper Custom object mapper. - * @return This object. - * @deprecated Use {@link #setJsonSerializer(JsonSerializer)}. - */ - @Deprecated(since = "0.11.0") - public ApiClient setObjectMapper(ObjectMapper mapper) { - return setJsonSerializer(new Jackson2JsonSerializer(mapper)); - } - - /** - * Get the current Jackson 2 object mapper. - * - * @return Current Jackson 2 object mapper. - * @throws UnsupportedOperationException if the active serializer does not use Jackson 2. - * @deprecated Use {@link #getJsonSerializer()}. - */ - @Deprecated(since = "0.11.0") - public ObjectMapper getObjectMapper() { - if (jsonSerializer instanceof Jackson2JsonSerializer) { - return ((Jackson2JsonSerializer) jsonSerializer).getObjectMapper(); - } - throw new UnsupportedOperationException("The active JSON serializer does not use Jackson 2"); - } - /** Set the serializer for request and response bodies. */ public ApiClient setJsonSerializer(JsonSerializer jsonSerializer) { this.jsonSerializer = Objects.requireNonNull(jsonSerializer, "JsonSerializer cannot be null"); diff --git a/src/main/java/dev/openfga/sdk/api/client/Jackson2JsonSerializer.java b/src/main/java/dev/openfga/sdk/api/client/Jackson2JsonSerializer.java deleted file mode 100644 index 0c74b017..00000000 --- a/src/main/java/dev/openfga/sdk/api/client/Jackson2JsonSerializer.java +++ /dev/null @@ -1,86 +0,0 @@ -package dev.openfga.sdk.api.client; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.openfga.sdk.errors.SdkSerializationException; -import java.io.IOException; -import org.openapitools.jackson.nullable.JsonNullableModule; - -/** Serializes SDK values with Jackson 2. */ -final class Jackson2JsonSerializer implements JsonSerializer { - private final ObjectMapper objectMapper; - - Jackson2JsonSerializer() { - this(createDefaultObjectMapper()); - } - - Jackson2JsonSerializer(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - ObjectMapper getObjectMapper() { - return objectMapper; - } - - @Override - public byte[] writeValueAsBytes(Object value) throws SdkSerializationException { - try { - return objectMapper.writeValueAsBytes(value); - } catch (IOException error) { - throw new SdkSerializationException("Cannot serialize JSON value", error); - } - } - - @Override - public T readValue(byte[] source, Class type) throws SdkSerializationException { - try { - return objectMapper.readValue(source, type); - } catch (IOException error) { - throw new SdkSerializationException("Cannot deserialize JSON value", error); - } - } - - @Override - public T readValue(String source, Class type) throws SdkSerializationException { - try { - return objectMapper.readValue(source, type); - } catch (IOException error) { - throw new SdkSerializationException("Cannot deserialize JSON value", error); - } - } - - @Override - public T readValue(byte[] source, SdkTypeToken type) throws SdkSerializationException { - try { - return objectMapper.readValue(source, objectMapper.getTypeFactory().constructType(type.getType())); - } catch (IOException error) { - throw new SdkSerializationException("Cannot deserialize JSON value", error); - } - } - - @Override - public T readValue(String source, SdkTypeToken type) throws SdkSerializationException { - try { - return objectMapper.readValue(source, objectMapper.getTypeFactory().constructType(type.getType())); - } catch (IOException error) { - throw new SdkSerializationException("Cannot deserialize JSON value", error); - } - } - - static ObjectMapper createDefaultObjectMapper() { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); - objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); - objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.registerModule(new JsonNullableModule()); - return objectMapper; - } -} diff --git a/src/main/java/dev/openfga/sdk/api/client/Jackson3JsonSerializer.java b/src/main/java/dev/openfga/sdk/api/client/Jackson3JsonSerializer.java new file mode 100644 index 00000000..3fb6fae1 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/Jackson3JsonSerializer.java @@ -0,0 +1,68 @@ +package dev.openfga.sdk.api.client; + +import com.fasterxml.jackson.annotation.JsonInclude; +import dev.openfga.sdk.errors.SdkSerializationException; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.cfg.EnumFeature; +import tools.jackson.databind.json.JsonMapper; + +/** Serializes SDK values with Jackson 3. */ +final class Jackson3JsonSerializer implements JsonSerializer { + private final JsonMapper objectMapper = JsonMapper.builderWithJackson2Defaults() + .changeDefaultPropertyInclusion(inclusion -> + JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE) + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DateTimeFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .enable(EnumFeature.WRITE_ENUMS_USING_TO_STRING) + .enable(EnumFeature.READ_ENUMS_USING_TO_STRING) + .build(); + + @Override + public byte[] writeValueAsBytes(Object value) throws SdkSerializationException { + try { + return objectMapper.writeValueAsBytes(value); + } catch (JacksonException error) { + throw new SdkSerializationException("Cannot serialize JSON value", error); + } + } + + @Override + public T readValue(byte[] source, Class type) throws SdkSerializationException { + try { + return objectMapper.readValue(source, type); + } catch (JacksonException error) { + throw new SdkSerializationException("Cannot deserialize JSON value", error); + } + } + + @Override + public T readValue(String source, Class type) throws SdkSerializationException { + try { + return objectMapper.readValue(source, type); + } catch (JacksonException error) { + throw new SdkSerializationException("Cannot deserialize JSON value", error); + } + } + + @Override + public T readValue(byte[] source, SdkTypeToken type) throws SdkSerializationException { + try { + return objectMapper.readValue(source, objectMapper.getTypeFactory().constructType(type.getType())); + } catch (JacksonException error) { + throw new SdkSerializationException("Cannot deserialize JSON value", error); + } + } + + @Override + public T readValue(String source, SdkTypeToken type) throws SdkSerializationException { + try { + return objectMapper.readValue(source, objectMapper.getTypeFactory().constructType(type.getType())); + } catch (JacksonException error) { + throw new SdkSerializationException("Cannot deserialize JSON value", error); + } + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/JsonSerializer.java b/src/main/java/dev/openfga/sdk/api/client/JsonSerializer.java index 6649f67f..9e3d066c 100644 --- a/src/main/java/dev/openfga/sdk/api/client/JsonSerializer.java +++ b/src/main/java/dev/openfga/sdk/api/client/JsonSerializer.java @@ -2,20 +2,63 @@ import dev.openfga.sdk.errors.SdkSerializationException; -/** Serializes SDK request and response values without exposing a JSON library. */ +/** + * Serializes SDK request and response values without exposing a JSON library. + * Implementations must support SDK model annotations and wrap JSON processing + * failures in {@link SdkSerializationException}, preserving the original cause. + */ public interface JsonSerializer { /** Creates the SDK default serializer. */ static JsonSerializer createDefault() { - return new Jackson2JsonSerializer(); + return new Jackson3JsonSerializer(); } + /** + * Encodes a request value as UTF-8 JSON. + * + * @param value Request value to encode. + * @return Encoded JSON bytes. + * @throws SdkSerializationException if the value cannot be encoded. + */ byte[] writeValueAsBytes(Object value) throws SdkSerializationException; + /** + * Decodes a UTF-8 JSON response into a non-generic type. + * + * @param source JSON response bytes. + * @param type Response class. + * @return Decoded response. + * @throws SdkSerializationException if the JSON is invalid or cannot map to the type. + */ T readValue(byte[] source, Class type) throws SdkSerializationException; + /** + * Decodes JSON text into a non-generic response type. + * + * @param source JSON response text. + * @param type Response class. + * @return Decoded response. + * @throws SdkSerializationException if the JSON is invalid or cannot map to the type. + */ T readValue(String source, Class type) throws SdkSerializationException; + /** + * Decodes a UTF-8 JSON response while preserving generic type arguments. + * + * @param source JSON response bytes. + * @param type Response type, including nested type arguments. + * @return Decoded response. + * @throws SdkSerializationException if the JSON is invalid or cannot map to the type. + */ T readValue(byte[] source, SdkTypeToken type) throws SdkSerializationException; + /** + * Decodes JSON text while preserving generic type arguments, including stream results. + * + * @param source JSON response text. + * @param type Response type, including nested type arguments. + * @return Decoded response. + * @throws SdkSerializationException if the JSON is invalid or cannot map to the type. + */ T readValue(String source, SdkTypeToken type) throws SdkSerializationException; } diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 5735abfa..a01a0386 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -3,7 +3,6 @@ import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; import static java.util.UUID.randomUUID; -import com.fasterxml.jackson.core.type.TypeReference; import dev.openfga.sdk.api.*; import dev.openfga.sdk.api.client.model.*; import dev.openfga.sdk.api.configuration.*; @@ -105,19 +104,6 @@ public StreamingApiExecutor streamingApiExecutor(SdkTypeToken(this.apiClient, this.configuration, type); } - /** - * Returns a streaming executor for a generic response type. - * - * @param The response object type - * @param typeRef Jackson type reference for {@code StreamResult} - * @return Streaming API executor - * @deprecated Use {@link #streamingApiExecutor(SdkTypeToken)}. - */ - @Deprecated(since = "0.11.0") - public StreamingApiExecutor streamingApiExecutor(TypeReference> typeRef) { - return new StreamingApiExecutor<>(this.apiClient, this.configuration, typeRef); - } - public void setStoreId(String storeId) { configuration.storeId(storeId); } diff --git a/src/main/java/dev/openfga/sdk/api/client/StreamingApiExecutor.java b/src/main/java/dev/openfga/sdk/api/client/StreamingApiExecutor.java index 89ddd18d..9766c998 100644 --- a/src/main/java/dev/openfga/sdk/api/client/StreamingApiExecutor.java +++ b/src/main/java/dev/openfga/sdk/api/client/StreamingApiExecutor.java @@ -1,6 +1,5 @@ package dev.openfga.sdk.api.client; -import com.fasterxml.jackson.core.type.TypeReference; import dev.openfga.sdk.api.BaseStreamingApi; import dev.openfga.sdk.api.configuration.Configuration; import dev.openfga.sdk.api.model.StreamResult; @@ -57,24 +56,6 @@ public StreamingApiExecutor(ApiClient apiClient, Configuration configuration, Sd requireNonNull(type, "SdkTypeToken cannot be null")); } - /** - * Use when the response type is generic. - * - * @param apiClient API client for HTTP operations - * @param configuration Client configuration - * @param typeRef Jackson type reference for {@code StreamResult} - * @deprecated Use {@link #StreamingApiExecutor(ApiClient, Configuration, SdkTypeToken)}. - */ - @Deprecated(since = "0.11.0") - public StreamingApiExecutor( - ApiClient apiClient, Configuration configuration, TypeReference> typeRef) { - this( - apiClient, - configuration, - SdkTypeToken.from( - requireNonNull(typeRef, "TypeReference cannot be null").getType())); - } - /** Throws {@link IllegalArgumentException} if {@code value} is null. */ private static V requireNonNull(V value, String message) { if (value == null) { diff --git a/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java index f8593455..51e43303 100644 --- a/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.*; import java.io.IOException; @@ -18,6 +16,9 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.openfga.OpenFGAContainer; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; @TestInstance(Lifecycle.PER_CLASS) @Testcontainers @@ -26,7 +27,8 @@ public class OpenFgaApiIntegrationTest { @Container private static final OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.10.2"); - private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private static final ObjectMapper mapper = + JsonMapper.builderWithJackson2Defaults().build(); private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; private static final String DEFAULT_DOC = "document:2021-budget"; private static final TupleKey DEFAULT_TUPLE_KEY = @@ -178,7 +180,7 @@ public void readAuthModels() throws Exception { assertEquals( "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"conditional_reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"name_starts_with_a\"}],\"module\":\"\",\"source_info\":null},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}],\"module\":\"\",\"source_info\":null},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}],\"module\":\"\",\"source_info\":null},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}],\"module\":\"\",\"source_info\":null}},\"module\":\"\",\"source_info\":null}}]", typeDefsJson); - } catch (JsonProcessingException ex) { + } catch (JacksonException ex) { assertNull(ex); } }); diff --git a/src/test-integration/java/dev/openfga/sdk/api/client/ApiExecutorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/client/ApiExecutorIntegrationTest.java index 780c2fc5..d94911db 100644 --- a/src/test-integration/java/dev/openfga/sdk/api/client/ApiExecutorIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/api/client/ApiExecutorIntegrationTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.configuration.ClientConfiguration; import dev.openfga.sdk.api.model.*; import java.util.HashMap; @@ -15,6 +14,8 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.openfga.OpenFGAContainer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; /** * Integration tests for ApiExecutor functionality. @@ -28,7 +29,8 @@ public class ApiExecutorIntegrationTest { @Container private static final OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.10.2"); - private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private static final ObjectMapper mapper = + JsonMapper.builderWithJackson2Defaults().build(); private OpenFgaClient fga; diff --git a/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java index 1501ba89..a82b78c3 100644 --- a/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.client.model.*; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.*; @@ -20,6 +18,9 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.openfga.OpenFGAContainer; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; @TestInstance(Lifecycle.PER_CLASS) @Testcontainers @@ -28,7 +29,8 @@ public class OpenFgaClientIntegrationTest { @Container private static final OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.10.2"); - private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private static final ObjectMapper mapper = + JsonMapper.builderWithJackson2Defaults().build(); private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; private static final String DEFAULT_DOC = "document:2021-budget"; private static final ClientTupleKeyWithoutCondition DEFAULT_TUPLE_KEY_NO_CONDITION = @@ -204,7 +206,7 @@ public void readAuthModels() throws Exception { assertEquals( "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"conditional_reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"name_starts_with_a\"}],\"module\":\"\",\"source_info\":null},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}],\"module\":\"\",\"source_info\":null},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}],\"module\":\"\",\"source_info\":null},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}],\"module\":\"\",\"source_info\":null}},\"module\":\"\",\"source_info\":null}}]", typeDefsJson); - } catch (JsonProcessingException ex) { + } catch (JacksonException ex) { assertNull(ex); } }); diff --git a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java index 21b7aec3..27cee2bf 100644 --- a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.client.OpenFgaClient; import dev.openfga.sdk.api.client.model.ClientTupleKey; import dev.openfga.sdk.api.client.model.ClientWriteRequest; @@ -22,6 +21,8 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.openfga.OpenFGAContainer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; @TestInstance(Lifecycle.PER_CLASS) @Testcontainers @@ -30,7 +31,8 @@ public class FgaErrorIntegrationTest { @Container private static final OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.10.2"); - private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private static final ObjectMapper mapper = + JsonMapper.builderWithJackson2Defaults().build(); // Test constants private static final String ERROR_CODE_VALIDATION_ERROR = "validation_error"; diff --git a/src/test-integration/java/dev/openfga/sdk/example/Example1.java b/src/test-integration/java/dev/openfga/sdk/example/Example1.java index 8a2e27f6..f9e82029 100644 --- a/src/test-integration/java/dev/openfga/sdk/example/Example1.java +++ b/src/test-integration/java/dev/openfga/sdk/example/Example1.java @@ -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.*; @@ -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 { @@ -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"); diff --git a/src/test/java/dev/openfga/sdk/LegacyStreamingApiTest.java b/src/test/java/dev/openfga/sdk/LegacyStreamingApiTest.java deleted file mode 100644 index da6786a2..00000000 --- a/src/test/java/dev/openfga/sdk/LegacyStreamingApiTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.openfga.sdk; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.sun.net.httpserver.HttpServer; -import dev.openfga.sdk.api.BaseStreamingApi; -import dev.openfga.sdk.api.client.ApiClient; -import dev.openfga.sdk.api.configuration.Configuration; -import dev.openfga.sdk.api.model.StreamResult; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.http.HttpRequest; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import org.junit.jupiter.api.Test; - -class LegacyStreamingApiTest { - @Test - void preservesLegacySubclassParsingAndStreaming() throws Exception { - HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); - server.createContext("/stream", exchange -> { - byte[] body = "{\"result\":[\"document:one\",\"document:two\"]}\n".getBytes(StandardCharsets.UTF_8); - exchange.sendResponseHeaders(200, body.length); - try (var output = exchange.getResponseBody()) { - output.write(body); - } - }); - server.start(); - try { - LegacyStreamingApi api = new LegacyStreamingApi(new ApiClient()); - assertEquals(List.of("document:one"), api.parse("{\"result\":[\"document:one\"]}")); - List> results = new ArrayList<>(); - api.stream(URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/stream"), results::add) - .get(5, TimeUnit.SECONDS); - assertEquals(List.of(List.of("document:one", "document:two")), results); - } finally { - server.stop(0); - } - } - - @SuppressWarnings("deprecation") - static class LegacyStreamingApi extends BaseStreamingApi> { - LegacyStreamingApi(ApiClient client) { - super(new Configuration(), client, new TypeReference>>() {}); - } - - List parse(String source) throws Exception { - return objectMapper.readValue(source, streamResultTypeRef).getResult(); - } - - CompletableFuture stream(URI uri, Consumer> consumer) { - return processStreamingResponse(HttpRequest.newBuilder(uri).build(), consumer, null); - } - } -} diff --git a/src/test/java/dev/openfga/sdk/TestJsonSerializer.java b/src/test/java/dev/openfga/sdk/TestJsonSerializer.java new file mode 100644 index 00000000..4a3daee2 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/TestJsonSerializer.java @@ -0,0 +1,35 @@ +package dev.openfga.sdk; + +import dev.openfga.sdk.api.client.JsonSerializer; +import dev.openfga.sdk.api.client.SdkTypeToken; +import tools.jackson.databind.json.JsonMapper; + +/** Keeps the custom mapper configuration used by HTTP request tests. */ +public final class TestJsonSerializer implements JsonSerializer { + private final JsonMapper mapper = JsonMapper.builderWithJackson2Defaults().build(); + + @Override + public byte[] writeValueAsBytes(Object value) { + return mapper.writeValueAsBytes(value); + } + + @Override + public T readValue(byte[] source, Class type) { + return mapper.readValue(source, type); + } + + @Override + public T readValue(String source, Class type) { + return mapper.readValue(source, type); + } + + @Override + public T readValue(byte[] source, SdkTypeToken type) { + return mapper.readValue(source, mapper.getTypeFactory().constructType(type.getType())); + } + + @Override + public T readValue(String source, SdkTypeToken type) { + return mapper.readValue(source, mapper.getTypeFactory().constructType(type.getType())); + } +} diff --git a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java index 8f58fd1a..9033ccdd 100644 --- a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java +++ b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java @@ -5,8 +5,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; import com.pgssoft.httpclient.HttpClientMock; +import dev.openfga.sdk.TestJsonSerializer; import dev.openfga.sdk.api.client.*; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.*; @@ -38,7 +38,6 @@ public class OpenFgaApiTest { private static final String EMPTY_RESPONSE_BODY = "{}"; private static final TelemetryConfiguration DEFAULT_TELEMETRY_CONFIG = new TelemetryConfiguration(); - private final ObjectMapper mapper = new ObjectMapper(); private OpenFgaApi fga; private Configuration mockConfiguration; private ApiClient mockApiClient; @@ -68,8 +67,7 @@ public void beforeEachTest() throws Exception { doNothing().when(mockConfiguration).assertValid(); mockApiClient = mock(ApiClient.class); - when(mockApiClient.getJsonSerializer()) - .thenReturn(new ApiClient().setObjectMapper(mapper).getJsonSerializer()); + when(mockApiClient.getJsonSerializer()).thenReturn(new TestJsonSerializer()); when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); when(mockApiClient.getHttpClientBuilder()).thenReturn(mockHttpClientBuilder); diff --git a/src/test/java/dev/openfga/sdk/api/StreamingApiTest.java b/src/test/java/dev/openfga/sdk/api/StreamingApiTest.java index 088bef98..7bc68c52 100644 --- a/src/test/java/dev/openfga/sdk/api/StreamingApiTest.java +++ b/src/test/java/dev/openfga/sdk/api/StreamingApiTest.java @@ -4,10 +4,12 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.client.ApiClient; +import dev.openfga.sdk.api.client.JsonSerializer; +import dev.openfga.sdk.api.client.SdkTypeToken; import dev.openfga.sdk.api.configuration.Configuration; import dev.openfga.sdk.api.model.ListObjectsRequest; +import dev.openfga.sdk.api.model.StreamResult; import dev.openfga.sdk.api.model.StreamedListObjectsResponse; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -46,15 +48,12 @@ class StreamingApiTest { private ApiClient mockApiClient; private StreamedListObjectsApi streamingApi; - private ObjectMapper objectMapper; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - objectMapper = new ObjectMapper(); - when(mockApiClient.getJsonSerializer()) - .thenReturn(new ApiClient().setObjectMapper(objectMapper).getJsonSerializer()); + when(mockApiClient.getJsonSerializer()).thenReturn(JsonSerializer.createDefault()); when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); when(mockConfiguration.getApiUrl()).thenReturn("https://api.fga.example"); @@ -233,21 +232,16 @@ void testStreamedListObjects_largeStream() throws Exception { @Test void testGenericStreamResult_deserialization() throws Exception { - // Test that StreamResult properly deserializes for different types - ObjectMapper mapper = new ObjectMapper(); - - // Test with result - String jsonWithResult = "{\"result\":{\"object\":\"document:1\"}}"; - var resultType = mapper.getTypeFactory() - .constructParametricType( - dev.openfga.sdk.api.model.StreamResult.class, StreamedListObjectsResponse.class); - - Object streamResult = mapper.readValue(jsonWithResult, resultType); - assertNotNull(streamResult); - - // Test with error - code should be an integer - String jsonWithError = "{\"error\":{\"code\":400,\"message\":\"Error occurred\"}}"; - Object streamResultWithError = mapper.readValue(jsonWithError, resultType); - assertNotNull(streamResultWithError); + JsonSerializer serializer = JsonSerializer.createDefault(); + SdkTypeToken> type = + new SdkTypeToken>() {}; + StreamResult result = + serializer.readValue("{\"result\":{\"object\":\"document:1\"}}", type); + assertEquals("document:1", result.getResult().getObject()); + + StreamResult error = + serializer.readValue("{\"error\":{\"code\":400,\"message\":\"Error occurred\"}}", type); + assertEquals(400, error.getError().getCode()); + assertEquals("Error occurred", error.getError().getMessage()); } } diff --git a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java index 51543d22..bfc54aa7 100644 --- a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java @@ -7,11 +7,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.junit5.*; import com.github.tomakehurst.wiremock.stubbing.Scenario; import com.pgssoft.httpclient.HttpClientMock; import dev.openfga.sdk.api.client.ApiClient; +import dev.openfga.sdk.api.client.JsonSerializer; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.constants.FgaConstants; import dev.openfga.sdk.errors.FgaInvalidParameterException; @@ -38,7 +38,6 @@ class OAuth2ClientTest { private static final String GRANT_TYPE = "client_credentials"; private static final String ACCESS_TOKEN = "0123456789"; - private final ObjectMapper mapper = new ObjectMapper(); private HttpClientMock mockHttpClient; private static Stream apiTokenIssuers() { @@ -422,8 +421,7 @@ private OAuth2Client newClientCredentialsClient( apiClient = mock(ApiClient.class); when(apiClient.getHttpClient()).thenReturn(mockHttpClient); - when(apiClient.getJsonSerializer()) - .thenReturn(new ApiClient().setObjectMapper(mapper).getJsonSerializer()); + when(apiClient.getJsonSerializer()).thenReturn(JsonSerializer.createDefault()); } else { apiClient = new ApiClient(); } diff --git a/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java b/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java index ea42544b..46a15109 100644 --- a/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java @@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.pgssoft.httpclient.HttpClientMock; import dev.openfga.sdk.api.configuration.ApiToken; import dev.openfga.sdk.api.configuration.ClientCredentials; @@ -17,6 +15,7 @@ import dev.openfga.sdk.api.configuration.Credentials; import dev.openfga.sdk.constants.FgaConstants; import dev.openfga.sdk.errors.ApiException; +import dev.openfga.sdk.errors.SdkSerializationException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -25,6 +24,9 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.json.JsonMapper; class ApiClientTest { @@ -59,47 +61,19 @@ public void customHttpClientWithHttp2() { } @Test - void objectMapperCustomizationAppliesThroughSerializer() throws Exception { - ObjectMapper objectMapper = new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); - ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), objectMapper); + void serializerCustomizationAppliesThroughClient() throws Exception { + ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), new SnakeCaseSerializer()); assertEquals( "{\"display_name\":\"example\"}", new String(apiClient.getJsonSerializer().writeValueAsBytes(new CustomPayload()), UTF_8)); - apiClient.setObjectMapper(new ObjectMapper()); + apiClient.setJsonSerializer(JsonSerializer.createDefault()); assertEquals( "{\"displayName\":\"example\"}", new String(apiClient.getJsonSerializer().writeValueAsBytes(new CustomPayload()), UTF_8)); } - @Test - void defaultConstructorHonorsObjectMapperFactoryOverride() throws Exception { - ApiClient apiClient = new SnakeCaseApiClient(); - - assertEquals( - "{\"display_name\":\"example\"}", - new String(apiClient.getJsonSerializer().writeValueAsBytes(new CustomPayload()), UTF_8)); - } - - @Test - void builderConstructorHonorsObjectMapperFactoryOverride() throws Exception { - ApiClient apiClient = new SnakeCaseApiClient(HttpClient.newBuilder()); - - assertEquals( - "{\"display_name\":\"example\"}", - new String(apiClient.getJsonSerializer().writeValueAsBytes(new CustomPayload()), UTF_8)); - } - - @Test - void objectMapperAccessorRejectsCustomSerializer() { - JsonSerializer serializer = Mockito.mock(JsonSerializer.class); - ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), serializer); - - assertEquals(serializer, apiClient.getJsonSerializer()); - assertThrows(UnsupportedOperationException.class, apiClient::getObjectMapper); - } - @Nested class ApplyAuthHeader { @@ -314,18 +288,34 @@ void clientCredentials_differentCredentials_exchangeSeparateTokens() throws Exce } } - private static class SnakeCaseApiClient extends ApiClient { - SnakeCaseApiClient() { - super(); + private static class SnakeCaseSerializer implements JsonSerializer { + private final ObjectMapper mapper = JsonMapper.builder() + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .build(); + + @Override + public byte[] writeValueAsBytes(Object value) throws SdkSerializationException { + return mapper.writeValueAsBytes(value); } - SnakeCaseApiClient(HttpClient.Builder builder) { - super(builder); + @Override + public T readValue(byte[] source, Class type) throws SdkSerializationException { + return mapper.readValue(source, type); + } + + @Override + public T readValue(String source, Class type) throws SdkSerializationException { + return mapper.readValue(source, type); + } + + @Override + public T readValue(byte[] source, SdkTypeToken type) throws SdkSerializationException { + return mapper.readValue(source, mapper.getTypeFactory().constructType(type.getType())); } @Override - protected ObjectMapper createDefaultObjectMapper() { - return super.createDefaultObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + public T readValue(String source, SdkTypeToken type) throws SdkSerializationException { + return mapper.readValue(source, mapper.getTypeFactory().constructType(type.getType())); } } diff --git a/src/test/java/dev/openfga/sdk/api/client/Jackson2JsonSerializerTest.java b/src/test/java/dev/openfga/sdk/api/client/Jackson2JsonSerializerTest.java deleted file mode 100644 index 4c259a38..00000000 --- a/src/test/java/dev/openfga/sdk/api/client/Jackson2JsonSerializerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.openfga.sdk.api.client; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import dev.openfga.sdk.api.model.CheckRequest; -import dev.openfga.sdk.api.model.CheckRequestTupleKey; -import dev.openfga.sdk.api.model.ConsistencyPreference; -import dev.openfga.sdk.api.model.Store; -import dev.openfga.sdk.api.model.StreamResult; -import dev.openfga.sdk.api.model.StreamedListObjectsResponse; -import java.nio.charset.StandardCharsets; -import java.time.OffsetDateTime; -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.openapitools.jackson.nullable.JsonNullable; - -class Jackson2JsonSerializerTest { - @Test - void writesGoldenJsonWithNullOmissionOrderAndEnumFormatting() throws Exception { - Jackson2JsonSerializer serializer = new Jackson2JsonSerializer(); - CheckRequest request = new CheckRequest() - .tupleKey(new CheckRequestTupleKey() - .user("user:anne") - .relation("viewer") - ._object("document:roadmap")) - .authorizationModelId("01H0FGA") - .context(Map.of("region", "us")) - .consistency(ConsistencyPreference.HIGHER_CONSISTENCY); - - assertArrayEquals( - "{\"tuple_key\":{\"user\":\"user:anne\",\"relation\":\"viewer\",\"object\":\"document:roadmap\"},\"authorization_model_id\":\"01H0FGA\",\"context\":{\"region\":\"us\"},\"consistency\":\"HIGHER_CONSISTENCY\"}" - .getBytes(StandardCharsets.UTF_8), - serializer.writeValueAsBytes(request)); - } - - @Test - void writesExplicitNullableRequiredValueAndOmitsOptionalNulls() throws Exception { - Jackson2JsonSerializer serializer = new Jackson2JsonSerializer(); - CheckRequest request = new CheckRequest(); - - assertArrayEquals( - "{\"tuple_key\":null,\"consistency\":\"UNSPECIFIED\"}".getBytes(StandardCharsets.UTF_8), - serializer.writeValueAsBytes(request)); - } - - @Test - void writesGoldenDateWithOriginalOffset() throws Exception { - Store store = new Store() - .id("store-id") - .name("store") - .createdAt(OffsetDateTime.parse("2026-09-14T12:34:56.123+02:00")) - .updatedAt(OffsetDateTime.parse("2026-09-14T10:35:00Z")); - - assertArrayEquals( - ("{\"id\":\"store-id\",\"name\":\"store\"," - + "\"created_at\":\"2026-09-14T12:34:56.123+02:00\"," - + "\"updated_at\":\"2026-09-14T10:35:00Z\"}") - .getBytes(StandardCharsets.UTF_8), - new Jackson2JsonSerializer().writeValueAsBytes(store)); - } - - @Test - void distinguishesUndefinedNullAndPresentNullableValues() throws Exception { - assertArrayEquals( - "{\"explicitNull\":null,\"present\":\"value\"}".getBytes(StandardCharsets.UTF_8), - new Jackson2JsonSerializer().writeValueAsBytes(new NullableValues())); - } - - @JsonPropertyOrder({"undefined", "explicitNull", "present"}) - static class NullableValues { - public JsonNullable undefined = JsonNullable.undefined(); - public JsonNullable explicitNull = JsonNullable.of(null); - public JsonNullable present = JsonNullable.of("value"); - } - - @Test - void readsGenericStreamResult() throws Exception { - Jackson2JsonSerializer serializer = new Jackson2JsonSerializer(); - SdkTypeToken> type = - new SdkTypeToken>() {}; - - StreamResult result = serializer.readValue( - "{\"result\":{\"object\":\"document:roadmap\"}}".getBytes(StandardCharsets.UTF_8), type); - - assertEquals("document:roadmap", result.getResult().getObject()); - } -} diff --git a/src/test/java/dev/openfga/sdk/api/client/Jackson3JsonSerializerTest.java b/src/test/java/dev/openfga/sdk/api/client/Jackson3JsonSerializerTest.java new file mode 100644 index 00000000..be05773a --- /dev/null +++ b/src/test/java/dev/openfga/sdk/api/client/Jackson3JsonSerializerTest.java @@ -0,0 +1,153 @@ +package dev.openfga.sdk.api.client; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import dev.openfga.sdk.api.model.CheckRequest; +import dev.openfga.sdk.api.model.CheckRequestTupleKey; +import dev.openfga.sdk.api.model.ConsistencyPreference; +import dev.openfga.sdk.api.model.Store; +import dev.openfga.sdk.api.model.StreamResult; +import dev.openfga.sdk.api.model.StreamedListObjectsResponse; +import dev.openfga.sdk.errors.SdkSerializationException; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class Jackson3JsonSerializerTest { + private final ObjectMapper baseline = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + + @Test + void writesGoldenJsonWithNullOmissionOrderAndEnumFormatting() throws Exception { + JsonSerializer serializer = JsonSerializer.createDefault(); + CheckRequest request = new CheckRequest() + .tupleKey(new CheckRequestTupleKey() + .user("user:anne") + .relation("viewer") + ._object("document:roadmap")) + .authorizationModelId("01H0FGA") + .context(Map.of("region", "us")) + .consistency(ConsistencyPreference.HIGHER_CONSISTENCY); + + assertArrayEquals( + "{\"tuple_key\":{\"user\":\"user:anne\",\"relation\":\"viewer\",\"object\":\"document:roadmap\"},\"authorization_model_id\":\"01H0FGA\",\"context\":{\"region\":\"us\"},\"consistency\":\"HIGHER_CONSISTENCY\"}" + .getBytes(StandardCharsets.UTF_8), + serializer.writeValueAsBytes(request)); + assertArrayEquals(baseline.writeValueAsBytes(request), serializer.writeValueAsBytes(request)); + } + + @Test + void writesExplicitNullableRequiredValueAndOmitsOptionalNulls() throws Exception { + JsonSerializer serializer = JsonSerializer.createDefault(); + CheckRequest request = new CheckRequest(); + + assertArrayEquals( + "{\"tuple_key\":null,\"consistency\":\"UNSPECIFIED\"}".getBytes(StandardCharsets.UTF_8), + serializer.writeValueAsBytes(request)); + assertArrayEquals(baseline.writeValueAsBytes(request), serializer.writeValueAsBytes(request)); + } + + @Test + void writesGoldenDateWithOriginalOffset() throws Exception { + Store store = new Store() + .id("store-id") + .name("store") + .createdAt(OffsetDateTime.parse("2026-09-14T12:34:56.123+02:00")) + .updatedAt(OffsetDateTime.parse("2026-09-14T10:35:00Z")); + + assertArrayEquals( + ("{\"id\":\"store-id\",\"name\":\"store\"," + + "\"created_at\":\"2026-09-14T12:34:56.123+02:00\"," + + "\"updated_at\":\"2026-09-14T10:35:00Z\"}") + .getBytes(StandardCharsets.UTF_8), + JsonSerializer.createDefault().writeValueAsBytes(store)); + assertArrayEquals( + baseline.writeValueAsBytes(store), + JsonSerializer.createDefault().writeValueAsBytes(store)); + } + + @Test + void readsGenericStreamResult() throws Exception { + JsonSerializer serializer = JsonSerializer.createDefault(); + SdkTypeToken> type = + new SdkTypeToken>() {}; + + StreamResult result = serializer.readValue( + "{\"result\":{\"object\":\"document:roadmap\"}}".getBytes(StandardCharsets.UTF_8), type); + + assertEquals("document:roadmap", result.getResult().getObject()); + com.fasterxml.jackson.core.type.TypeReference> baselineType = + new com.fasterxml.jackson.core.type.TypeReference>() {}; + assertEquals( + baseline.readValue("{\"result\":{\"object\":\"document:roadmap\"}}", baselineType) + .getResult(), + result.getResult()); + } + + @Test + void readsDatesAndUnknownFieldsLikeJackson2() throws Exception { + String json = "{\"id\":\"store-id\",\"name\":\"store\"," + + "\"created_at\":\"2026-09-14T12:34:56.123+02:00\",\"future_field\":true}"; + Store actual = JsonSerializer.createDefault().readValue(json, Store.class); + assertEquals(baseline.readValue(json, Store.class), actual); + assertEquals(OffsetDateTime.parse("2026-09-14T12:34:56.123+02:00"), actual.getCreatedAt()); + } + + @Test + void exposesMalformedJsonAsSdkSerializationException() { + JsonSerializer serializer = JsonSerializer.createDefault(); + byte[] malformed = "{".getBytes(StandardCharsets.UTF_8); + SdkTypeToken> type = new SdkTypeToken>() {}; + + assertNotNull(assertThrows(SdkSerializationException.class, () -> serializer.readValue("{", Store.class)) + .getCause()); + assertNotNull(assertThrows(SdkSerializationException.class, () -> serializer.readValue(malformed, Store.class)) + .getCause()); + assertNotNull(assertThrows(SdkSerializationException.class, () -> serializer.readValue("{", type)) + .getCause()); + assertNotNull(assertThrows(SdkSerializationException.class, () -> serializer.readValue(malformed, type)) + .getCause()); + } + + @Test + void readsConcreteResponseFromBytes() throws Exception { + Store store = JsonSerializer.createDefault() + .readValue("{\"id\":\"store-id\",\"name\":\"store\"}".getBytes(StandardCharsets.UTF_8), Store.class); + + assertEquals(new Store().id("store-id").name("store"), store); + } + + @Test + void preservesGetterFailureWhenSerializationFails() { + IllegalStateException failure = new IllegalStateException("Cannot read payload"); + Object payload = new Object() { + public String getValue() { + throw failure; + } + }; + + SdkSerializationException error = + assertThrows(SdkSerializationException.class, () -> JsonSerializer.createDefault() + .writeValueAsBytes(payload)); + + assertNotNull(error.getCause()); + assertSame(failure, error.getCause().getCause()); + } +} diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java index 9e14d2e9..0166fa6b 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java @@ -5,8 +5,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; import com.pgssoft.httpclient.HttpClientMock; +import dev.openfga.sdk.TestJsonSerializer; import dev.openfga.sdk.api.client.model.*; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.*; @@ -55,7 +55,7 @@ public void beforeEachTest() throws Exception { var mockApiClient = mock(ApiClient.class); when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); - when(mockApiClient.getJsonSerializer()).thenReturn(new Jackson2JsonSerializer(new ObjectMapper())); + when(mockApiClient.getJsonSerializer()).thenReturn(new TestJsonSerializer()); when(mockApiClient.getHttpClientBuilder()).thenReturn(mockHttpClientBuilder); fga = new OpenFgaClient(clientConfiguration, mockApiClient); diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java index f0685cf9..c9dc1579 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java @@ -8,12 +8,12 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.http.Fault; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.pgssoft.httpclient.HttpClientMock; +import dev.openfga.sdk.TestJsonSerializer; import dev.openfga.sdk.api.client.model.*; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.*; @@ -94,7 +94,7 @@ public void beforeEachTest() throws Exception { .maxRetries(FgaConstants.DEFAULT_MAX_RETRY) .minimumRetryDelay(FgaConstants.DEFAULT_MIN_WAIT_IN_MS); - fga = new OpenFgaClient(clientConfiguration, new ApiClient(mockHttpClientBuilder, new ObjectMapper())); + fga = new OpenFgaClient(clientConfiguration, new ApiClient(mockHttpClientBuilder, new TestJsonSerializer())); } /* ****************** @@ -2236,7 +2236,7 @@ private OpenFgaClient clientBackedByPendingResponses(List list, int expected) throws InterruptedException { diff --git a/src/test/java/dev/openfga/sdk/api/client/StreamedListObjectsTest.java b/src/test/java/dev/openfga/sdk/api/client/StreamedListObjectsTest.java index 9455b382..450400e8 100644 --- a/src/test/java/dev/openfga/sdk/api/client/StreamedListObjectsTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/StreamedListObjectsTest.java @@ -4,7 +4,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.client.model.ClientListObjectsRequest; import dev.openfga.sdk.api.client.model.ClientStreamedListObjectsOptions; import dev.openfga.sdk.api.configuration.ClientConfiguration; @@ -55,7 +54,7 @@ public void beforeEachTest() throws Exception { mockApiClient = mock(ApiClient.class); when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); - when(mockApiClient.getJsonSerializer()).thenReturn(new Jackson2JsonSerializer(new ObjectMapper())); + when(mockApiClient.getJsonSerializer()).thenReturn(JsonSerializer.createDefault()); when(mockApiClient.getHttpClientBuilder()).thenReturn(mockHttpClientBuilder); fga = new OpenFgaClient(clientConfiguration, mockApiClient); diff --git a/src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java b/src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java index 5fd7efe2..c85b781c 100644 --- a/src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java @@ -4,8 +4,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.openfga.sdk.api.configuration.ApiToken; import dev.openfga.sdk.api.configuration.ClientConfiguration; import dev.openfga.sdk.api.configuration.Credentials; @@ -60,7 +58,7 @@ public void beforeEachTest() throws Exception { mockApiClient = mock(ApiClient.class); when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); - when(mockApiClient.getJsonSerializer()).thenReturn(new Jackson2JsonSerializer(new ObjectMapper())); + when(mockApiClient.getJsonSerializer()).thenReturn(JsonSerializer.createDefault()); when(mockApiClient.getHttpClientBuilder()).thenReturn(mockHttpClientBuilder); fga = new OpenFgaClient(clientConfiguration, mockApiClient); @@ -306,24 +304,6 @@ public void streamingApiExecutor_throwsForNullResponseType() { assertThrows(IllegalArgumentException.class, () -> fga.streamingApiExecutor((Class) null)); } - @Test - public void streamingApiExecutor_typeReferenceOverload_works() throws Exception { - TypeReference> typeRef = - new TypeReference>() {}; - - Stream lines = Stream.of("{\"result\":{\"object\":\"document:1\"}}"); - HttpResponse> mockResponse = mockStreamResponse(200, lines); - when(mockHttpClient.>sendAsync(any(), any())) - .thenReturn(CompletableFuture.completedFuture(mockResponse)); - - List received = new ArrayList<>(); - fga.streamingApiExecutor(typeRef).stream(buildStreamedListObjectsRequest(), received::add) - .get(); - - assertEquals(1, received.size()); - assertEquals("document:1", received.get(0).getObject()); - } - @Test public void streamingApiExecutor_sdkTypeTokenOverload_works() throws Exception { SdkTypeToken> type =