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();
+ }
+ }
+}