Skip to content

Commit

Permalink
Jackson: also detect class referenced by @JsonTypeIdResolver
Browse files Browse the repository at this point in the history
Adds missing support for classes referenced by `@JsonTypeIdResolver`, which is particularly important when using native builds.

The added integration tests are likely only meaningful when run in native mode (`-Pnative`), otherwise "good old Java reflection" does its job.

Fixes quarkusio#27346
  • Loading branch information
snazy authored and sheilamjones committed Aug 22, 2022
1 parent f1e4782 commit 4041a2d
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 1 deletion.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
}
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit 4041a2d

Please sign in to comment.