Skip to content

Commit

Permalink
Merge pull request #25597 from geoand/jackson-mixin
Browse files Browse the repository at this point in the history
Introduce declarative support for Jackson's mixin feature
  • Loading branch information
gsmet committed Jun 9, 2022
2 parents 39769b4 + 6a58c30 commit 37ed0a2
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/src/main/asciidoc/rest-json.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ public class CustomObjectMapper {
}
----

===== Mixin support

Quarkus automates the registration of Jackson's Mixin support, via the `io.quarkus.jackson.JacksonMixin` annotation.
This annotation can be placed on classes that are meant to be used as Jackson mixins while the classes they are meant to customize
are defined as the value of the annotation.

==== JSON-B

As stated above, Quarkus provides the option of using JSON-B instead of Jackson via the use of the `quarkus-resteasy-jsonb` extension.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import static org.jboss.jandex.AnnotationTarget.Kind.METHOD;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
Expand All @@ -14,6 +17,7 @@
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Type;
Expand All @@ -31,12 +35,15 @@
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
Expand All @@ -46,7 +53,9 @@
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.jackson.JacksonMixin;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.jackson.runtime.MixinsRecorder;
import io.quarkus.jackson.runtime.ObjectMapperProducer;
import io.quarkus.jackson.spi.ClassPathJacksonModuleBuildItem;
import io.quarkus.jackson.spi.JacksonModuleBuildItem;
Expand All @@ -72,6 +81,7 @@ public class JacksonProcessor {
private static final String JDK8_MODULE = "com.fasterxml.jackson.datatype.jdk8.Jdk8Module";

private static final String PARAMETER_NAMES_MODULE = "com.fasterxml.jackson.module.paramnames.ParameterNamesModule";
private static final DotName JACKSON_MIXIN = DotName.createSimple(JacksonMixin.class.getName());

// this list can probably be enriched with more modules
private static final List<String> MODULES_NAMES_TO_AUTO_REGISTER = Arrays.asList(TIME_MODULE, JDK8_MODULE,
Expand Down Expand Up @@ -322,4 +332,49 @@ void generateCustomizer(BuildProducer<GeneratedBeanBuildItem> generatedBeans,
}
}
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
public void supportMixins(MixinsRecorder recorder,
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
IndexView index = combinedIndexBuildItem.getIndex();
Collection<AnnotationInstance> jacksonMixins = index.getAnnotations(JACKSON_MIXIN);
if (jacksonMixins.isEmpty()) {
return;
}

Map<Class<?>, Class<?>> mixinsMap = new HashMap<>();
for (AnnotationInstance instance : jacksonMixins) {
if (instance.target().kind() != CLASS) {
continue;
}
ClassInfo mixinClassInfo = instance.target().asClass();
String mixinClassName = mixinClassInfo.name().toString();
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, mixinClassName));
try {
Type[] targetTypes = instance.value().asClassArray();
if ((targetTypes == null) || targetTypes.length == 0) {
continue;
}
Class<?> mixinClass = Thread.currentThread().getContextClassLoader().loadClass(mixinClassName);
for (Type targetType : targetTypes) {
String targetClassName = targetType.name().toString();
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, targetClassName));
mixinsMap.put(Thread.currentThread().getContextClassLoader().loadClass(targetClassName),
mixinClass);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to determine Jackson mixin usage at build", e);
}
}
if (mixinsMap.isEmpty()) {
return;
}
syntheticBeans.produce(SyntheticBeanBuildItem.configure(ObjectMapperCustomizer.class)
.scope(Singleton.class)
.supplier(recorder.customizerSupplier(mixinsMap))
.done());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.quarkus.jackson.deployment;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.jackson.JacksonMixin;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.test.QuarkusUnitTest;

public class JacksonMixinsWithCustomizerTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest();

@Inject
ObjectMapper objectMapper;

@Test
public void test() throws JsonProcessingException {
assertThat(objectMapper.writeValueAsString(new Fruit("test"))).isEqualTo("{\"manual\":\"test\"}");
assertThat(objectMapper.writeValueAsString(new Message("hello"))).isEqualTo("{}");
}

@Singleton
static class TestCustomizer implements ObjectMapperCustomizer {

@Override
public void customize(ObjectMapper objectMapper) {
objectMapper.addMixIn(Fruit.class, ManualFruitMixin.class);
}
}

public static class Fruit {
public String name;

public Fruit(String name) {
this.name = name;
}
}

@JacksonMixin(Fruit.class)
public abstract static class AutoFruitMixin {
@JsonProperty("auto")
public String name;
}

// this mixin will override the AutoFruitMixin because it will be explicitly registered by the user with a customizer
public abstract static class ManualFruitMixin {
@JsonProperty("manual")
public String name;
}

public static class Message {
private final String description;

public Message(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}

@JacksonMixin(Message.class)
public interface MessageMixin {
@JsonIgnore
String getDescription();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.jackson.deployment;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import javax.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.jackson.JacksonMixin;
import io.quarkus.test.QuarkusUnitTest;

public class JacksonMixinsWithoutCustomizerTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest();

@Inject
ObjectMapper objectMapper;

@Test
public void test() throws JsonProcessingException {
assertThat(objectMapper.writeValueAsString(new Fruit("test"))).isEqualTo("{\"nm\":\"test\"}");
assertThat(objectMapper.writeValueAsString(new Fruit2("test"))).isEqualTo("{\"nm\":\"test\"}");
}

public static class Fruit {
public String name;

public Fruit(String name) {
this.name = name;
}
}

public static class Fruit2 {
public String name;

public Fruit2(String name) {
this.name = name;
}
}

@JacksonMixin({ Fruit.class, Fruit2.class })
public abstract static class FruitMixin {
@JsonProperty("nm")
public String name;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.jackson;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Used on classes that are meant to be used as Jackson mixins.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface JacksonMixin {

/**
* The types for which the mixin should apply to.
*/
Class<?>[] value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.jackson.runtime;

import java.util.Map;
import java.util.function.Supplier;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.runtime.annotations.Recorder;

@Recorder
public class MixinsRecorder {

public Supplier<ObjectMapperCustomizer> customizerSupplier(Map<Class<?>, Class<?>> mixinsMap) {
return new Supplier<>() {
@Override
public ObjectMapperCustomizer get() {
return new ObjectMapperCustomizer() {
@Override
public void customize(ObjectMapper objectMapper) {
for (var entry : mixinsMap.entrySet()) {
objectMapper.addMixIn(entry.getKey(), entry.getValue());
}
}

@Override
public int priority() {
return DEFAULT_PRIORITY + 1;
}
};
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public ModelWithJsonNamingStrategyResource(ObjectMapper objectMapper) {
@GET
@Produces(MediaType.APPLICATION_JSON)
public String get() throws IOException {
return objectMapper.writeValueAsString(new SampleResponse("My blog post"));
return objectMapper.writeValueAsString(new SampleResponse("My blog post", "best"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import io.quarkus.runtime.annotations.RegisterForReflection;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@RegisterForReflection
public class SampleResponse {

private String blogTitle;
private String name;

public SampleResponse() {
}

public SampleResponse(String blogTitle) {
public SampleResponse(String blogTitle, String name) {
this.blogTitle = blogTitle;
this.name = name;
}

public String getBlogTitle() {
Expand All @@ -26,10 +25,19 @@ public void setBlogTitle(String blogTitle) {
this.blogTitle = blogTitle;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "SampleResponse{" +
"blogTitle='" + blogTitle + '\'' +
", name='" + name + '\'' +
'}';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.it.jackson.model;

import com.fasterxml.jackson.annotation.JsonProperty;

import io.quarkus.jackson.JacksonMixin;

@JacksonMixin(SampleResponse.class)
public abstract class SampleResponseMixin {

@JsonProperty("nm")
public abstract String getName();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void testStrategy() throws IOException {
.when().get("/json-naming/")
.then()
.statusCode(200)
.body(containsString("blog_title"));
.body(containsString("blog_title"), containsString("nm"));
}

}

0 comments on commit 37ed0a2

Please sign in to comment.