Skip to content

Commit

Permalink
Support JsonInclude.Include in reflection free Jackson serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
mariofusco committed Sep 23, 2024
1 parent 13f979a commit 1f6f30c
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ public boolean isPublicField() {
}

private Type fieldType() {
if (fieldInfo != null) {
if (isPublicField()) {
return fieldInfo.type();
}
if (methodInfo.name().startsWith("set")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,23 +191,20 @@ protected boolean createSerializationMethod(ClassInfo classInfo, ClassCreator cl
private boolean serializeObject(ClassInfo classInfo, ClassCreator classCreator, String beanClassName,
MethodCreator serialize) {
Set<String> serializedFields = new HashSet<>();
ResultHandle valueHandle = serialize.checkCast(serialize.getMethodParam(0), beanClassName);
ResultHandle jsonGenerator = serialize.getMethodParam(1);
ResultHandle serializerProvider = serialize.getMethodParam(2);
SerializationContext ctx = new SerializationContext(serialize, beanClassName);

// jsonGenerator.writeStartObject();
MethodDescriptor writeStartObject = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeStartObject", "void");
serialize.invokeVirtualMethod(writeStartObject, jsonGenerator);
serialize.invokeVirtualMethod(writeStartObject, ctx.jsonGenerator);

boolean valid = serializeObjectData(classInfo, classCreator, serialize, valueHandle, jsonGenerator, serializerProvider,
serializedFields);
boolean valid = serializeObjectData(classInfo, classCreator, serialize, ctx, serializedFields);

// jsonGenerator.writeEndObject();
MethodDescriptor writeEndObject = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeEndObject", "void");
serialize.invokeVirtualMethod(writeEndObject, jsonGenerator);
serialize.invokeVirtualMethod(writeEndObject, ctx.jsonGenerator);

if (serializedFields.isEmpty()) {
throwExceptionForEmptyBean(beanClassName, serialize, jsonGenerator);
throwExceptionForEmptyBean(beanClassName, serialize, ctx.jsonGenerator);
}

classCreator.getMethodCreator("<clinit>", void.class).setModifiers(ACC_STATIC).returnVoid();
Expand All @@ -216,40 +213,34 @@ private boolean serializeObject(ClassInfo classInfo, ClassCreator classCreator,
}

private boolean serializeObjectData(ClassInfo classInfo, ClassCreator classCreator, MethodCreator serialize,
ResultHandle valueHandle, ResultHandle jsonGenerator, ResultHandle serializerProvider,
Set<String> serializedFields) {
return serializeFields(classInfo, classCreator, serialize, valueHandle, jsonGenerator, serializerProvider,
serializedFields) &&
serializeMethods(classInfo, classCreator, serialize, valueHandle, jsonGenerator, serializerProvider,
serializedFields);
SerializationContext ctx, Set<String> serializedFields) {
return serializeFields(classInfo, classCreator, serialize, ctx, serializedFields) &&
serializeMethods(classInfo, classCreator, serialize, ctx, serializedFields);
}

private boolean serializeFields(ClassInfo classInfo, ClassCreator classCreator, MethodCreator serialize,
ResultHandle valueHandle, ResultHandle jsonGenerator, ResultHandle serializerProvider,
Set<String> serializedFields) {
SerializationContext ctx, Set<String> serializedFields) {
for (FieldInfo fieldInfo : classFields(classInfo)) {
FieldSpecs fieldSpecs = fieldSpecsFromField(classInfo, fieldInfo);
if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) {
if (fieldSpecs.hasUnknownAnnotation()) {
return false;
}
writeField(classInfo, fieldSpecs, writeFieldBranch(classCreator, serialize, fieldSpecs), jsonGenerator,
serializerProvider, valueHandle);
writeField(classInfo, fieldSpecs, writeFieldBranch(classCreator, serialize, fieldSpecs), ctx);
}
}
return true;
}

private boolean serializeMethods(ClassInfo classInfo, ClassCreator classCreator, MethodCreator serialize,
ResultHandle valueHandle, ResultHandle jsonGenerator, ResultHandle serializerProvider,
Set<String> serializedFields) {
SerializationContext ctx, Set<String> serializedFields) {
for (MethodInfo methodInfo : classMethods(classInfo)) {
FieldSpecs fieldSpecs = fieldSpecsFromMethod(methodInfo);
if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) {
if (fieldSpecs.hasUnknownAnnotation()) {
return false;
}
writeField(classInfo, fieldSpecs, serialize, jsonGenerator, serializerProvider, valueHandle);
writeField(classInfo, fieldSpecs, serialize, ctx);
}
}
return true;
Expand All @@ -266,30 +257,39 @@ private boolean isGetterMethod(MethodInfo methodInfo) {
&& (methodName.startsWith("get") || methodName.startsWith("is"));
}

private void writeField(ClassInfo classInfo, FieldSpecs fieldSpecs, BytecodeCreator bytecode, ResultHandle jsonGenerator,
ResultHandle serializerProvider, ResultHandle valueHandle) {
private void writeField(ClassInfo classInfo, FieldSpecs fieldSpecs, BytecodeCreator bytecode, SerializationContext ctx) {
String pkgName = classInfo.name().packagePrefixName().toString();
generatedFields.computeIfAbsent(pkgName, pkg -> new HashSet<>()).add(fieldSpecs.jsonName);

ResultHandle arg = fieldSpecs.toValueReaderHandle(bytecode, valueHandle);
ResultHandle arg = fieldSpecs.toValueReaderHandle(bytecode, ctx.valueHandle);
bytecode = checkInclude(bytecode, ctx, arg);

String typeName = fieldSpecs.fieldType.name().toString();
String primitiveMethodName = writeMethodForPrimitiveFields(typeName);

if (primitiveMethodName != null) {
BytecodeCreator primitiveBytecode = isBoxedPrimitive(typeName) ? bytecode.ifNotNull(arg).trueBranch() : bytecode;
writeFieldName(fieldSpecs, primitiveBytecode, jsonGenerator, pkgName);
writeFieldName(fieldSpecs, primitiveBytecode, ctx.jsonGenerator, pkgName);
MethodDescriptor primitiveWriter = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, primitiveMethodName, "void",
fieldSpecs.writtenType());
primitiveBytecode.invokeVirtualMethod(primitiveWriter, jsonGenerator, arg);
primitiveBytecode.invokeVirtualMethod(primitiveWriter, ctx.jsonGenerator, arg);
return;
}

registerTypeToBeGenerated(fieldSpecs.fieldType, typeName);

writeFieldName(fieldSpecs, bytecode, jsonGenerator, pkgName);
writeFieldName(fieldSpecs, bytecode, ctx.jsonGenerator, pkgName);
MethodDescriptor writeMethod = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writePOJO",
void.class, Object.class);
bytecode.invokeVirtualMethod(writeMethod, jsonGenerator, arg);
bytecode.invokeVirtualMethod(writeMethod, ctx.jsonGenerator, arg);
}

private static BytecodeCreator checkInclude(BytecodeCreator bytecode, SerializationContext ctx, ResultHandle arg) {
MethodDescriptor shouldSerialize = MethodDescriptor.ofMethod(JacksonMapperUtil.SerializationInclude.class,
"shouldSerialize",
boolean.class, Object.class);
ResultHandle included = bytecode.invokeVirtualMethod(shouldSerialize, ctx.includeHandle, arg);
return bytecode.ifTrue(included).trueBranch();
}

private static void writeFieldName(FieldSpecs fieldSpecs, BytecodeCreator bytecode, ResultHandle jsonGenerator,
Expand Down Expand Up @@ -376,4 +376,22 @@ private void throwExceptionForEmptyBean(String beanClassName, MethodCreator seri
isFailEnabledBranch.load(errorMsg), javaType);
isFailEnabledBranch.throwException(invalidException);
}

private record SerializationContext(ResultHandle valueHandle, ResultHandle jsonGenerator, ResultHandle serializerProvider,
ResultHandle includeHandle) {
SerializationContext(MethodCreator serialize, String beanClassName) {
this(valueHandle(serialize, beanClassName), serialize.getMethodParam(1), serialize.getMethodParam(2),
includeHandle(serialize));
}

private static ResultHandle valueHandle(MethodCreator serialize, String beanClassName) {
return serialize.checkCast(serialize.getMethodParam(0), beanClassName);
}

private static ResultHandle includeHandle(MethodCreator serialize) {
MethodDescriptor decodeInclude = MethodDescriptor.ofMethod(JacksonMapperUtil.SerializationInclude.class, "decode",
JacksonMapperUtil.SerializationInclude.class, Object.class, SerializerProvider.class);
return serialize.invokeStaticMethod(decodeInclude, serialize.getMethodParam(0), serialize.getMethodParam(2));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization;
import io.smallrye.common.annotation.NonBlocking;

@Path("/json-include")
@NonBlocking
@DisableSecureSerialization
public class JsonIncludeTestResource {

@GET
@Path("/my-object-empty")
public MyObject getEmptyObject() {
return new MyObject();
}

@GET
@Path("/my-object")
public MyObject getObject() {
MyObject myObject = new MyObject();
myObject.setName("name");
myObject.setDescription("description");
myObject.setStrings("test");
myObject.getMap().put("test", 1);
return myObject;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class MyObject {

private String name;

private String description;

private Map<String, Integer> map = new HashMap<>();

private String[] strings = new String[0];

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

public void setDescription(String description) {
this.description = description;
}

public String getName() {
return name;
}

public Optional<String> getDescription() {
return Optional.ofNullable(description);
}

public Map<String, Integer> getMap() {
return map;
}

public void setMap(Map<String, Integer> map) {
this.map = map;
}

public String[] getStrings() {
return strings;
}

public void setStrings(String... strings) {
this.strings = strings;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.function.Supplier;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class NonAbsentReflectionFreeSerializationTest extends NonAbsentSerializationTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(JsonIncludeTestResource.class, MyObject.class, NonAbsentObjectMapperCustomizer.class)
.addAsResource(
new StringAsset(
"quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true\n"),
"application.properties");
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.function.Supplier;

import jakarta.inject.Singleton;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class NonAbsentSerializationTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(JsonIncludeTestResource.class, MyObject.class, NonAbsentObjectMapperCustomizer.class)
.addAsResource(new StringAsset(""), "application.properties");
}
});

@Singleton
public static class NonAbsentObjectMapperCustomizer implements ObjectMapperCustomizer {

@Override
public void customize(ObjectMapper objectMapper) {
objectMapper
.enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
}
}

@Test
public void testObject() {
RestAssured.get("/json-include/my-object")
.then()
.statusCode(200)
.contentType("application/json")
.body("name", Matchers.equalTo("name"))
.body("description", Matchers.equalTo("description"))
.body("map.test", Matchers.equalTo(1))
.body("strings[0]", Matchers.equalTo("test"));
}

@Test
public void testEmptyObject() {
RestAssured.get("/json-include/my-object-empty")
.then()
.statusCode(200)
.contentType("application/json")
.body("name", Matchers.nullValue())
.body("description", Matchers.nullValue())
.body("map", Matchers.anEmptyMap())
.body("strings", Matchers.hasSize(0));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.function.Supplier;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class NonEmptyReflectionFreeSerializationTest extends NonEmptySerializationTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(JsonIncludeTestResource.class, MyObject.class, NonEmptyObjectMapperCustomizer.class)
.addAsResource(
new StringAsset(
"quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true\n"),
"application.properties");
}
});
}
Loading

0 comments on commit 1f6f30c

Please sign in to comment.