diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index e957b0a1002e2..a89809f5ed8b1 100644 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import com.fasterxml.jackson.databind.module.SimpleModule; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -68,6 +69,8 @@ public class JacksonProcessor { private static final DotName JSON_AUTO_DETECT = DotName.createSimple(JsonAutoDetect.class.getName()); + private static final DotName JSON_TYPE_ID_RESOLVER = DotName.createSimple(JsonTypeIdResolver.class.getName()); + private static final DotName JSON_CREATOR = DotName.createSimple("com.fasterxml.jackson.annotation.JsonCreator"); private static final DotName JSON_NAMING = DotName.createSimple("com.fasterxml.jackson.databind.annotation.JsonNaming"); @@ -185,13 +188,28 @@ void register( } for (AnnotationInstance creatorInstance : index.getAnnotations(JSON_AUTO_DETECT)) { - if (creatorInstance.target().kind().equals(CLASS)) { + if (creatorInstance.target().kind() == CLASS) { reflectiveClass .produce( new ReflectiveClassBuildItem(true, true, creatorInstance.target().asClass().name().toString())); } } + // Register @JsonTypeIdResolver implementations for reflection. + // Note: @JsonTypeIdResolver is, simply speaking, the "dynamic version" of @JsonSubTypes, i.e. sub-types are + // dynamically identified by Jackson's `TypeIdResolver.typeFromId()`, which returns sub-types of the annotated + // class. Means: the referenced `TypeIdResolver` _and_ all sub-types of the annotated class must be registered + // for reflection. + for (AnnotationInstance resolverInstance : index.getAnnotations(JSON_TYPE_ID_RESOLVER)) { + AnnotationValue value = resolverInstance.value("value"); + if (value != null) { + // Add the type-id-resolver class + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, value.asClass().name().toString())); + // Add the whole hierarchy of the annotated class + addReflectiveHierarchyClass(resolverInstance.target().asClass().name(), reflectiveHierarchyClass); + } + } + // make sure we register the constructors and methods marked with @JsonCreator for reflection for (AnnotationInstance creatorInstance : index.getAnnotations(JSON_CREATOR)) { if (METHOD == creatorInstance.target().kind()) { diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverResource.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverResource.java new file mode 100644 index 0000000000000..e358cf1f716ff --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverResource.java @@ -0,0 +1,46 @@ +package io.quarkus.it.jackson; + +import java.io.IOException; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.it.jackson.model.ModelWithJsonTypeIdResolver; + +@Path("/typeIdResolver") +public class ModelWithJsonTypeIdResolverResource { + + private final ObjectMapper objectMapper; + + public ModelWithJsonTypeIdResolverResource(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_JSON) + public String post(String body) throws IOException { + ModelWithJsonTypeIdResolver input = objectMapper.readValue(body, ModelWithJsonTypeIdResolver.class); + return input.getType(); + } + + @GET + @Path("one") + @Produces(MediaType.APPLICATION_JSON) + public String one() throws IOException { + return objectMapper.writeValueAsString(new ModelWithJsonTypeIdResolver.SubclassOne()); + } + + @GET + @Path("two") + @Produces(MediaType.APPLICATION_JSON) + public String two() throws IOException { + return objectMapper.writeValueAsString(new ModelWithJsonTypeIdResolver.SubclassTwo()); + } +} diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/CustomTypeResolver.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/CustomTypeResolver.java new file mode 100644 index 0000000000000..1a1d594567704 --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/CustomTypeResolver.java @@ -0,0 +1,56 @@ +package io.quarkus.it.jackson.model; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import com.fasterxml.jackson.databind.type.TypeFactory; + +public class CustomTypeResolver extends TypeIdResolverBase { + + private JavaType baseType; + + public CustomTypeResolver() { + } + + @Override + public void init(JavaType bt) { + baseType = bt; + } + + @Override + public String idFromValue(Object value) { + return getId(value); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + return getId(value); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } + + private String getId(Object value) { + if (value instanceof ModelWithJsonTypeIdResolver) { + return ((ModelWithJsonTypeIdResolver) value).getType(); + } + + return null; + } + + @Override + public JavaType typeFromId(DatabindContext context, String id) { + if (id != null) { + switch (id) { + case "ONE": + return context.constructSpecializedType(baseType, ModelWithJsonTypeIdResolver.SubclassOne.class); + case "TWO": + return context.constructSpecializedType(baseType, ModelWithJsonTypeIdResolver.SubclassTwo.class); + } + } + return TypeFactory.unknownType(); + } +} diff --git a/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/ModelWithJsonTypeIdResolver.java b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/ModelWithJsonTypeIdResolver.java new file mode 100644 index 0000000000000..088a73847db6e --- /dev/null +++ b/integration-tests/jackson/src/main/java/io/quarkus/it/jackson/model/ModelWithJsonTypeIdResolver.java @@ -0,0 +1,36 @@ +package io.quarkus.it.jackson.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; + +@JsonTypeIdResolver(CustomTypeResolver.class) +@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") +public abstract class ModelWithJsonTypeIdResolver { + + public ModelWithJsonTypeIdResolver() { + } + + @JsonIgnore + public abstract String getType(); + + public static class SubclassOne extends ModelWithJsonTypeIdResolver { + public SubclassOne() { + } + + @Override + public String getType() { + return "ONE"; + } + } + + public static class SubclassTwo extends ModelWithJsonTypeIdResolver { + public SubclassTwo() { + } + + @Override + public String getType() { + return "TWO"; + } + } +} diff --git a/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverIT.java b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverIT.java new file mode 100644 index 0000000000000..d80f67cb1f190 --- /dev/null +++ b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverIT.java @@ -0,0 +1,9 @@ +package io.quarkus.it.jackson; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ModelWithJsonTypeIdResolverIT extends ModelWithJsonTypeIdResolverTest { + + // Execute the same tests but in native mode. +} diff --git a/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverTest.java b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverTest.java new file mode 100644 index 0000000000000..e22c1f885e567 --- /dev/null +++ b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/ModelWithJsonTypeIdResolverTest.java @@ -0,0 +1,57 @@ +package io.quarkus.it.jackson; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.it.jackson.model.ModelWithJsonTypeIdResolver; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class ModelWithJsonTypeIdResolverTest { + + static List typeIds() { + return Arrays.asList( + new ModelWithJsonTypeIdResolver.SubclassOne(), + new ModelWithJsonTypeIdResolver.SubclassTwo()); + } + + @ParameterizedTest + @MethodSource("typeIds") + public void testPost(ModelWithJsonTypeIdResolver instance) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + + given() + .contentType("application/json") + .body(objectMapper.writeValueAsString(instance)) + .when().post("/typeIdResolver") + .then() + .statusCode(200) + .body(is(instance.getType())); + } + + static List types() { + return Arrays.asList( + Arguments.arguments("one", "ONE"), + Arguments.arguments("two", "TWO")); + } + + @ParameterizedTest + @MethodSource("types") + public void testGets(String endpoint, String expectedType) { + given().when().get("/typeIdResolver/" + endpoint) + .then() + .statusCode(200) + .body("type", equalTo(expectedType)); + } +}