Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jackson: also detect class referenced by @JsonTypeIdResolver #27357

Merged
merged 1 commit into from Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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()) {
Expand Down
@@ -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());
}
}
@@ -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();
}
}
@@ -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";
}
}
}
@@ -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.
}
@@ -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<ModelWithJsonTypeIdResolver> 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<Arguments> 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));
}
}