This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 0043a70fd8 [api] Support external Jackson for simple REST requests 
(#9369)
0043a70fd8 is described below

commit 0043a70fd88ac75dcb83a8f2da5e72ce91e22b1f
Author: Stefan Wang <[email protected]>
AuthorDate: Mon Aug 24 01:29:32 2026 -0400

    [api] Support external Jackson for simple REST requests (#9369)
---
 paimon-api/pom.xml                                 |   7 +
 .../paimon/rest/requests/AlterDatabaseRequest.java |   2 +
 .../rest/requests/AuthTableQueryRequest.java       |   2 +
 .../paimon/rest/requests/CreateBranchRequest.java  |   3 +
 .../rest/requests/CreateDatabaseRequest.java       |   2 +
 .../paimon/rest/requests/CreateTagRequest.java     |   3 +
 .../rest/requests/DropPartitionsRequest.java       |   2 +
 .../requests/ListPartitionsByFilterRequest.java    |   8 +
 .../requests/ListPartitionsByNamesRequest.java     |   2 +
 .../rest/requests/MarkDonePartitionsRequest.java   |   2 +
 .../paimon/rest/requests/ResetConsumerRequest.java |   3 +
 .../rest/requests/RollbackSchemaRequest.java       |   3 +
 .../requests/RequestJacksonCompatibilityTest.java  | 351 +++++++++++++++++++++
 13 files changed, 390 insertions(+)

diff --git a/paimon-api/pom.xml b/paimon-api/pom.xml
index 1a121ad73b..1c37347455 100644
--- a/paimon-api/pom.xml
+++ b/paimon-api/pom.xml
@@ -60,6 +60,13 @@ under the License.
             <artifactId>httpclient5</artifactId>
             <version>${apache.hc.client.version}</version>
         </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${paimon.shade.jackson.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java
index 4c1ea03cf2..97019e3229 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/AlterDatabaseRequest.java
@@ -25,6 +25,7 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGet
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
+import java.beans.ConstructorProperties;
 import java.util.List;
 import java.util.Map;
 
@@ -42,6 +43,7 @@ public class AlterDatabaseRequest implements RESTRequest {
     private final Map<String, String> updates;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_REMOVALS, FIELD_UPDATES})
     public AlterDatabaseRequest(
             @JsonProperty(FIELD_REMOVALS) List<String> removals,
             @JsonProperty(FIELD_UPDATES) Map<String, String> updates) {
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/AuthTableQueryRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/AuthTableQueryRequest.java
index 2e05821835..40fb5016c1 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/AuthTableQueryRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/AuthTableQueryRequest.java
@@ -27,6 +27,7 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 
 import javax.annotation.Nullable;
 
+import java.beans.ConstructorProperties;
 import java.util.List;
 
 /** Request for auth table query. */
@@ -40,6 +41,7 @@ public class AuthTableQueryRequest implements RESTRequest {
     private final List<String> select;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_SELECT})
     public AuthTableQueryRequest(@JsonProperty(FIELD_SELECT) @Nullable 
List<String> select) {
         this.select = select;
     }
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateBranchRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateBranchRequest.java
index 394807d6ff..20b25d5723 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateBranchRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateBranchRequest.java
@@ -27,6 +27,8 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 
 import javax.annotation.Nullable;
 
+import java.beans.ConstructorProperties;
+
 /** Request for creating branch. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class CreateBranchRequest implements RESTRequest {
@@ -42,6 +44,7 @@ public class CreateBranchRequest implements RESTRequest {
     private final String fromTag;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_BRANCH, FIELD_FROM_TAG})
     public CreateBranchRequest(
             @JsonProperty(FIELD_BRANCH) String branch,
             @Nullable @JsonProperty(FIELD_FROM_TAG) String fromTag) {
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
index 29f607786e..af550d0983 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateDatabaseRequest.java
@@ -25,6 +25,7 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGet
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
+import java.beans.ConstructorProperties;
 import java.util.Map;
 
 /** Request for creating database. */
@@ -41,6 +42,7 @@ public class CreateDatabaseRequest implements RESTRequest {
     private final Map<String, String> options;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_NAME, FIELD_OPTIONS})
     public CreateDatabaseRequest(
             @JsonProperty(FIELD_NAME) String name,
             @JsonProperty(FIELD_OPTIONS) Map<String, String> options) {
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateTagRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateTagRequest.java
index 49cc0f20b7..19d9aeef1d 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateTagRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/CreateTagRequest.java
@@ -27,6 +27,8 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 
 import javax.annotation.Nullable;
 
+import java.beans.ConstructorProperties;
+
 /** Request for creating tag. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class CreateTagRequest implements RESTRequest {
@@ -47,6 +49,7 @@ public class CreateTagRequest implements RESTRequest {
     private final String timeRetained;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_TAG_NAME, FIELD_SNAPSHOT_ID, 
FIELD_TIME_RETAINED})
     public CreateTagRequest(
             @JsonProperty(FIELD_TAG_NAME) String tagName,
             @Nullable @JsonProperty(FIELD_SNAPSHOT_ID) Long snapshotId,
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/DropPartitionsRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/DropPartitionsRequest.java
index cdc448034e..08d5e560d6 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/DropPartitionsRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/DropPartitionsRequest.java
@@ -27,6 +27,7 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 
 import javax.annotation.Nullable;
 
+import java.beans.ConstructorProperties;
 import java.util.List;
 import java.util.Map;
 
@@ -48,6 +49,7 @@ public class DropPartitionsRequest implements RESTRequest {
     }
 
     @JsonCreator
+    @ConstructorProperties({FIELD_PARTITION_SPECS, FIELD_IGNORE_IF_NOT_EXISTS})
     public DropPartitionsRequest(
             @JsonProperty(FIELD_PARTITION_SPECS) List<Map<String, String>> 
partitionSpecs,
             @JsonProperty(FIELD_IGNORE_IF_NOT_EXISTS) @Nullable Boolean 
ignoreIfNotExists) {
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
index cd0fa058c4..102b51e11d 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
@@ -28,6 +28,8 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 
 import javax.annotation.Nullable;
 
+import java.beans.ConstructorProperties;
+
 /** Request for listing partitions by filter. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(JsonInclude.Include.NON_NULL)
@@ -55,6 +57,12 @@ public class ListPartitionsByFilterRequest implements 
RESTRequest {
     private final String pageToken;
 
     @JsonCreator
+    @ConstructorProperties({
+        FIELD_FILTER,
+        FIELD_PARTITION_NAME_PATTERN,
+        FIELD_MAX_RESULTS,
+        FIELD_PAGE_TOKEN
+    })
     public ListPartitionsByFilterRequest(
             @JsonProperty(FIELD_FILTER) String filter,
             @JsonProperty(FIELD_PARTITION_NAME_PATTERN) @Nullable String 
partitionNamePattern,
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByNamesRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByNamesRequest.java
index 7cec810769..cf41d2ff87 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByNamesRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByNamesRequest.java
@@ -22,6 +22,7 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCre
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
+import java.beans.ConstructorProperties;
 import java.util.List;
 import java.util.Map;
 
@@ -30,6 +31,7 @@ import java.util.Map;
 public class ListPartitionsByNamesRequest extends BasePartitionsRequest {
 
     @JsonCreator
+    @ConstructorProperties({FIELD_PARTITION_SPECS})
     public ListPartitionsByNamesRequest(
             @JsonProperty(FIELD_PARTITION_SPECS) List<Map<String, String>> 
partitionSpecs) {
         super(partitionSpecs);
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/MarkDonePartitionsRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/MarkDonePartitionsRequest.java
index 88345e9620..d908dba6b9 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/MarkDonePartitionsRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/MarkDonePartitionsRequest.java
@@ -22,6 +22,7 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCre
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
+import java.beans.ConstructorProperties;
 import java.util.List;
 import java.util.Map;
 
@@ -30,6 +31,7 @@ import java.util.Map;
 public class MarkDonePartitionsRequest extends BasePartitionsRequest {
 
     @JsonCreator
+    @ConstructorProperties({FIELD_PARTITION_SPECS})
     public MarkDonePartitionsRequest(
             @JsonProperty(FIELD_PARTITION_SPECS) List<Map<String, String>> 
partitionSpecs) {
         super(partitionSpecs);
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ResetConsumerRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ResetConsumerRequest.java
index 15bc8d375e..49610773a3 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ResetConsumerRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ResetConsumerRequest.java
@@ -27,6 +27,8 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 
 import javax.annotation.Nullable;
 
+import java.beans.ConstructorProperties;
+
 /** Request for resetting consumer. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class ResetConsumerRequest implements RESTRequest {
@@ -42,6 +44,7 @@ public class ResetConsumerRequest implements RESTRequest {
     private final Long nextSnapshotId;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_CONSUMER_ID, FIELD_NEXT_SNAPSHOT_ID})
     public ResetConsumerRequest(
             @JsonProperty(FIELD_CONSUMER_ID) String consumerId,
             @Nullable @JsonProperty(FIELD_NEXT_SNAPSHOT_ID) Long 
nextSnapshotId) {
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackSchemaRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackSchemaRequest.java
index f35d22ca80..f1a1b54314 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackSchemaRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackSchemaRequest.java
@@ -25,6 +25,8 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGet
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
+import java.beans.ConstructorProperties;
+
 /** Request for rollback table schema. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class RollbackSchemaRequest implements RESTRequest {
@@ -35,6 +37,7 @@ public class RollbackSchemaRequest implements RESTRequest {
     private final long schemaId;
 
     @JsonCreator
+    @ConstructorProperties({FIELD_SCHEMA_ID})
     public RollbackSchemaRequest(@JsonProperty(FIELD_SCHEMA_ID) long schemaId) 
{
         this.schemaId = schemaId;
     }
diff --git 
a/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
 
b/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
new file mode 100644
index 0000000000..51bf7c8831
--- /dev/null
+++ 
b/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest.requests;
+
+import org.apache.paimon.rest.RESTApi;
+import org.apache.paimon.rest.RESTRequest;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.beans.ConstructorProperties;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests request DTOs with Paimon's shaded and external Jackson mappers. */
+public class RequestJacksonCompatibilityTest {
+
+    private static final com.fasterxml.jackson.databind.ObjectMapper 
EXTERNAL_MAPPER =
+            new com.fasterxml.jackson.databind.ObjectMapper();
+
+    private static final List<RequestCase<?>> SIMPLE_REQUESTS =
+            Arrays.asList(
+                    requestCase(
+                            AlterDatabaseRequest.class,
+                            
"{\"removals\":[\"owner\"],\"updates\":{\"comment\":\"analytics\"}}",
+                            request -> {
+                                
assertThat(request.getRemovals()).containsExactly("owner");
+                                assertThat(request.getUpdates())
+                                        .containsEntry("comment", "analytics");
+                            },
+                            "removals",
+                            "updates"),
+                    requestCase(
+                            AuthTableQueryRequest.class,
+                            "{\"select\":[\"id\",\"name\"]}",
+                            request -> 
assertThat(request.select()).containsExactly("id", "name"),
+                            "select"),
+                    requestCase(
+                            CreateBranchRequest.class,
+                            "{\"branch\":\"audit\",\"fromTag\":\"v1\"}",
+                            request -> {
+                                
assertThat(request.branch()).isEqualTo("audit");
+                                assertThat(request.fromTag()).isEqualTo("v1");
+                            },
+                            "branch",
+                            "fromTag"),
+                    requestCase(
+                            CreateDatabaseRequest.class,
+                            
"{\"name\":\"warehouse\",\"options\":{\"owner\":\"alice\"}}",
+                            request -> {
+                                
assertThat(request.getName()).isEqualTo("warehouse");
+                                
assertThat(request.getOptions()).containsEntry("owner", "alice");
+                            },
+                            "name",
+                            "options"),
+                    requestCase(
+                            CreateTagRequest.class,
+                            
"{\"tagName\":\"v2\",\"snapshotId\":42,\"timeRetained\":\"7 d\"}",
+                            request -> {
+                                assertThat(request.tagName()).isEqualTo("v2");
+                                
assertThat(request.snapshotId()).isEqualTo(42L);
+                                
assertThat(request.timeRetained()).isEqualTo("7 d");
+                            },
+                            "tagName",
+                            "snapshotId",
+                            "timeRetained"),
+                    requestCase(
+                            DropPartitionsRequest.class,
+                            "{\"partitionSpecs\":[{\"dt\":\"2026-08-24\"}],"
+                                    + "\"ignoreIfNotExists\":false}",
+                            request -> {
+                                assertThat(request.getPartitionSpecs())
+                                        .containsExactly(
+                                                Collections.singletonMap("dt", 
"2026-08-24"));
+                                
assertThat(request.ignoreIfNotExists()).isFalse();
+                            },
+                            "partitionSpecs",
+                            "ignoreIfNotExists"),
+                    requestCase(
+                            ListPartitionsByFilterRequest.class,
+                            "{\"filter\":\"dt = '2026-08-24'\","
+                                    + "\"partitionNamePattern\":\"dt=*\","
+                                    + 
"\"maxResults\":25,\"pageToken\":\"next\"}",
+                            request -> {
+                                assertThat(request.getFilter()).isEqualTo("dt 
= '2026-08-24'");
+                                
assertThat(request.getPartitionNamePattern()).isEqualTo("dt=*");
+                                
assertThat(request.getMaxResults()).isEqualTo(25);
+                                
assertThat(request.getPageToken()).isEqualTo("next");
+                            },
+                            "filter",
+                            "partitionNamePattern",
+                            "maxResults",
+                            "pageToken"),
+                    requestCase(
+                            ListPartitionsByNamesRequest.class,
+                            "{\"specs\":[{\"dt\":\"2026-08-24\"}]}",
+                            request ->
+                                    assertThat(request.getPartitionSpecs())
+                                            .containsExactly(
+                                                    
Collections.singletonMap("dt", "2026-08-24")),
+                            "specs"),
+                    requestCase(
+                            MarkDonePartitionsRequest.class,
+                            "{\"specs\":[{\"dt\":\"2026-08-24\"}]}",
+                            request ->
+                                    assertThat(request.getPartitionSpecs())
+                                            .containsExactly(
+                                                    
Collections.singletonMap("dt", "2026-08-24")),
+                            "specs"),
+                    requestCase(
+                            ResetConsumerRequest.class,
+                            "{\"consumerId\":\"etl\",\"nextSnapshotId\":43}",
+                            request -> {
+                                
assertThat(request.consumerId()).isEqualTo("etl");
+                                
assertThat(request.nextSnapshotId()).isEqualTo(43L);
+                            },
+                            "consumerId",
+                            "nextSnapshotId"),
+                    requestCase(
+                            RollbackSchemaRequest.class,
+                            "{\"schemaId\":44}",
+                            request -> 
assertThat(request.getSchemaId()).isEqualTo(44L),
+                            "schemaId"));
+
+    private static final Set<Class<? extends RESTRequest>> COMPLEX_REQUESTS =
+            Stream.<Class<? extends RESTRequest>>of(
+                            AlterFunctionRequest.class,
+                            AlterTableRequest.class,
+                            AlterViewRequest.class,
+                            CommitTableRequest.class,
+                            CreateFunctionRequest.class,
+                            CreatePartitionsRequest.class,
+                            CreateTableRequest.class,
+                            CreateViewRequest.class,
+                            RegisterTableRequest.class,
+                            RenameTableRequest.class,
+                            ReplaceTableRequest.class,
+                            RollbackTableRequest.class)
+                    .collect(Collectors.toSet());
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("simpleRequests")
+    void testExternalJacksonDeserializesEveryField(RequestCase<?> requestCase) 
throws Exception {
+        requestCase.assertFields(requestCase.read(EXTERNAL_MAPPER));
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("simpleRequests")
+    void testShadedJacksonRoundTrips(RequestCase<?> requestCase) throws 
Exception {
+        RESTRequest request = requestCase.read(EXTERNAL_MAPPER);
+        requestCase.assertFields(
+                RESTApi.OBJECT_MAPPER.readValue(
+                        RESTApi.OBJECT_MAPPER.writeValueAsString(request), 
requestCase.type));
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("simpleRequests")
+    void testConstructorPropertyNamesAndOrder(RequestCase<?> requestCase) {
+        Constructor<?> creator = jsonCreator(requestCase.type);
+
+        assertThat(creator.getAnnotation(ConstructorProperties.class))
+                .isNotNull()
+                .extracting(ConstructorProperties::value)
+                .isEqualTo(requestCase.propertyNames);
+    }
+
+    @Test
+    void testRequestCreatorAllowlistsAreComplete() throws Exception {
+        Set<Class<? extends RESTRequest>> simpleRequests =
+                SIMPLE_REQUESTS.stream()
+                        .map(requestCase -> requestCase.type)
+                        .collect(Collectors.toSet());
+        Set<Class<? extends RESTRequest>> expected = new 
LinkedHashSet<>(simpleRequests);
+        expected.addAll(COMPLEX_REQUESTS);
+
+        
assertThat(findConcreteRequestCreators()).containsExactlyInAnyOrderElementsOf(expected);
+        assertThat(simpleRequests)
+                .allSatisfy(
+                        type ->
+                                
assertThat(jsonCreator(type).getGenericParameterTypes())
+                                        .allMatch(
+                                                RequestJacksonCompatibilityTest
+                                                        
::isOrdinaryJacksonType));
+        assertThat(COMPLEX_REQUESTS)
+                .allSatisfy(
+                        type ->
+                                
assertThat(jsonCreator(type).getGenericParameterTypes())
+                                        .anyMatch(parameter -> 
!isOrdinaryJacksonType(parameter)));
+    }
+
+    @Test
+    void testUnknownPropertiesFollowExternalMapperConfiguration() throws 
Exception {
+        String json = 
"{\"name\":\"warehouse\",\"options\":{},\"unknown\":true}";
+
+        assertThatThrownBy(() -> EXTERNAL_MAPPER.readValue(json, 
CreateDatabaseRequest.class))
+                .isInstanceOf(
+                        
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.class);
+
+        com.fasterxml.jackson.databind.ObjectMapper lenientMapper =
+                new com.fasterxml.jackson.databind.ObjectMapper()
+                        .configure(
+                                
com.fasterxml.jackson.databind.DeserializationFeature
+                                        .FAIL_ON_UNKNOWN_PROPERTIES,
+                                false);
+        assertThat(lenientMapper.readValue(json, 
CreateDatabaseRequest.class).getName())
+                .isEqualTo("warehouse");
+    }
+
+    private static Stream<RequestCase<?>> simpleRequests() {
+        return SIMPLE_REQUESTS.stream();
+    }
+
+    private static Constructor<?> jsonCreator(Class<?> type) {
+        return Arrays.stream(type.getDeclaredConstructors())
+                .filter(constructor -> 
constructor.isAnnotationPresent(JsonCreator.class))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("No @JsonCreator 
constructor on " + type));
+    }
+
+    private static Set<Class<? extends RESTRequest>> 
findConcreteRequestCreators()
+            throws Exception {
+        String packageName = 
RequestJacksonCompatibilityTest.class.getPackage().getName();
+        Path packagePath =
+                Paths.get(
+                                RESTRequest.class
+                                        .getProtectionDomain()
+                                        .getCodeSource()
+                                        .getLocation()
+                                        .toURI())
+                        .resolve(packageName.replace('.', '/'));
+
+        try (Stream<Path> paths = Files.list(packagePath)) {
+            Set<Class<? extends RESTRequest>> requests = new LinkedHashSet<>();
+            for (Path path :
+                    paths.filter(file -> 
file.getFileName().toString().endsWith("Request.class"))
+                            .collect(Collectors.toList())) {
+                String className =
+                        packageName
+                                + "."
+                                + 
path.getFileName().toString().replaceFirst("\\.class$", "");
+                Class<?> type = Class.forName(className);
+                if (RESTRequest.class.isAssignableFrom(type)
+                        && !Modifier.isAbstract(type.getModifiers())
+                        && Arrays.stream(type.getDeclaredConstructors())
+                                .anyMatch(
+                                        constructor ->
+                                                
constructor.isAnnotationPresent(
+                                                        JsonCreator.class))) {
+                    requests.add(type.asSubclass(RESTRequest.class));
+                }
+            }
+            return requests;
+        }
+    }
+
+    private static boolean isOrdinaryJacksonType(Type type) {
+        if (type instanceof Class<?>) {
+            Class<?> clazz = (Class<?>) type;
+            return clazz.isPrimitive()
+                    || clazz == String.class
+                    || clazz == Boolean.class
+                    || clazz == Character.class
+                    || Number.class.isAssignableFrom(clazz);
+        }
+        if (!(type instanceof ParameterizedType)) {
+            return false;
+        }
+
+        ParameterizedType parameterizedType = (ParameterizedType) type;
+        Class<?> rawType = (Class<?>) parameterizedType.getRawType();
+        Type[] arguments = parameterizedType.getActualTypeArguments();
+        if (Collection.class.isAssignableFrom(rawType)) {
+            return arguments.length == 1 && 
isOrdinaryJacksonType(arguments[0]);
+        }
+        return Map.class.isAssignableFrom(rawType)
+                && arguments.length == 2
+                && arguments[0] == String.class
+                && isOrdinaryJacksonType(arguments[1]);
+    }
+
+    private static <T extends RESTRequest> RequestCase<T> requestCase(
+            Class<T> type, String json, Consumer<T> assertions, String... 
propertyNames) {
+        return new RequestCase<>(type, json, assertions, propertyNames);
+    }
+
+    private static class RequestCase<T extends RESTRequest> {
+
+        private final Class<T> type;
+        private final String json;
+        private final Consumer<T> assertions;
+        private final String[] propertyNames;
+
+        private RequestCase(
+                Class<T> type, String json, Consumer<T> assertions, String[] 
propertyNames) {
+            this.type = type;
+            this.json = json;
+            this.assertions = assertions;
+            this.propertyNames = propertyNames;
+        }
+
+        private T read(com.fasterxml.jackson.databind.ObjectMapper mapper) 
throws Exception {
+            return mapper.readValue(json, type);
+        }
+
+        private void assertFields(RESTRequest request) {
+            assertions.accept(type.cast(request));
+        }
+
+        @Override
+        public String toString() {
+            return type.getSimpleName();
+        }
+    }
+}

Reply via email to