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

Support JsonInclude.Include in reflection free Jackson serializer #43443

Merged
merged 1 commit into from
Sep 25, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading