Skip to content

Commit f346fe6

Browse files
authored
Merge pull request #47662 from mariofusco/q47652
Fix Jackson serializers generation for Kotlin data classes
2 parents 4c0d835 + 836c578 commit f346fe6

File tree

8 files changed

+101
-36
lines changed

8 files changed

+101
-36
lines changed

extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java

+32-8
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,30 @@ private MethodInfo getterMethodInfo(ClassInfo classInfo, FieldInfo fieldInfo) {
230230
return findMethod(classInfo, methodName);
231231
}
232232

233-
protected FieldSpecs fieldSpecsFromField(ClassInfo classInfo, FieldInfo fieldInfo) {
233+
protected Optional<MethodInfo> findConstructor(ClassInfo classInfo) {
234+
Optional<MethodInfo> ctorOpt = classInfo.constructors().stream()
235+
.filter(ctor -> Modifier.isPublic(ctor.flags()) && ctor.hasAnnotation(JsonCreator.class))
236+
.findFirst();
237+
238+
if (ctorOpt.isEmpty()) {
239+
if (classInfo.hasNoArgsConstructor() && !classInfo.isRecord()) {
240+
return classInfo.constructors().stream()
241+
.filter(ctor -> ctor.parametersCount() == 0)
242+
.findFirst();
243+
}
244+
ctorOpt = classInfo.isRecord() ? Optional.of(classInfo.canonicalRecordConstructor())
245+
: classInfo.constructors().stream().filter(ctor -> Modifier.isPublic(ctor.flags())).findFirst();
246+
}
247+
return ctorOpt;
248+
}
249+
250+
protected FieldSpecs fieldSpecsFromField(ClassInfo classInfo, MethodInfo constructor, FieldInfo fieldInfo) {
234251
if (Modifier.isStatic(fieldInfo.flags())) {
235252
return null;
236253
}
237254
MethodInfo getterMethodInfo = getterMethodInfo(classInfo, fieldInfo);
238255
if (getterMethodInfo != null) {
239-
return new FieldSpecs(fieldInfo, getterMethodInfo);
256+
return new FieldSpecs(constructor, fieldInfo, getterMethodInfo);
240257
}
241258
if (Modifier.isPublic(fieldInfo.flags())) {
242259
return new FieldSpecs(fieldInfo);
@@ -260,14 +277,14 @@ protected static class FieldSpecs {
260277
FieldInfo fieldInfo;
261278

262279
FieldSpecs(FieldInfo fieldInfo) {
263-
this(fieldInfo, null);
280+
this(null, fieldInfo, null);
264281
}
265282

266283
FieldSpecs(MethodInfo methodInfo) {
267-
this(null, methodInfo);
284+
this(null, null, methodInfo);
268285
}
269286

270-
FieldSpecs(FieldInfo fieldInfo, MethodInfo methodInfo) {
287+
FieldSpecs(MethodInfo constructor, FieldInfo fieldInfo, MethodInfo methodInfo) {
271288
if (fieldInfo != null) {
272289
this.fieldInfo = fieldInfo;
273290
readAnnotations(fieldInfo);
@@ -278,14 +295,14 @@ protected static class FieldSpecs {
278295
}
279296
this.fieldType = fieldType();
280297
this.fieldName = fieldName();
281-
this.jsonName = jsonName();
298+
this.jsonName = jsonName(constructor);
282299
}
283300

284301
FieldSpecs(MethodParameterInfo paramInfo) {
285302
readAnnotations(paramInfo);
286303
this.fieldType = paramInfo.type();
287304
this.fieldName = paramInfo.name();
288-
this.jsonName = jsonName();
305+
this.jsonName = jsonName(null);
289306
}
290307

291308
private void readAnnotations(AnnotationTarget target) {
@@ -306,8 +323,15 @@ private Type fieldType() {
306323
return methodInfo.returnType();
307324
}
308325

309-
private String jsonName() {
326+
private String jsonName(MethodInfo constructor) {
310327
AnnotationInstance jsonProperty = annotations.get(JsonProperty.class.getName());
328+
if (jsonProperty == null && constructor != null) {
329+
jsonProperty = constructor.parameters().stream()
330+
.filter(parameter -> parameter.name().equals(fieldName)).findFirst()
331+
.map(parameter -> parameter.annotation(JsonProperty.class.getName()))
332+
.orElse(null);
333+
}
334+
311335
if (jsonProperty != null) {
312336
AnnotationValue value = jsonProperty.value();
313337
if (value != null && !value.asString().isEmpty()) {

extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java

+19-25
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.jboss.jandex.TypeVariable;
2626
import org.jboss.jandex.VoidType;
2727

28-
import com.fasterxml.jackson.annotation.JsonCreator;
2928
import com.fasterxml.jackson.core.JacksonException;
3029
import com.fasterxml.jackson.core.JsonParser;
3130
import com.fasterxml.jackson.core.ObjectCodec;
@@ -219,9 +218,19 @@ protected boolean createSerializationMethod(ClassInfo classInfo, ClassCreator cl
219218
.addException(IOException.class)
220219
.addException(JacksonException.class);
221220

222-
DeserializationData deserData = new DeserializationData(classInfo, classCreator, deserialize,
221+
Optional<MethodInfo> ctorOpt = findConstructor(classInfo);
222+
if (ctorOpt.isEmpty()) {
223+
return false;
224+
}
225+
226+
MethodInfo ctor = ctorOpt.get();
227+
DeserializationData deserData = new DeserializationData(classInfo, ctor, classCreator, deserialize,
223228
getJsonNode(deserialize), parseTypeParameters(classInfo, classCreator), new HashSet<>());
224-
ResultHandle deserializedHandle = createDeserializedObject(deserData);
229+
230+
ResultHandle deserializedHandle = ctor.parametersCount() == 0
231+
? deserData.methodCreator.newInstance(MethodDescriptor.ofConstructor(deserData.classInfo.name().toString()))
232+
: createDeserializedObject(deserData);
233+
225234
if (deserializedHandle == null) {
226235
return false;
227236
}
@@ -242,26 +251,9 @@ private static ResultHandle getJsonNode(MethodCreator deserialize) {
242251
}
243252

244253
private ResultHandle createDeserializedObject(DeserializationData deserData) {
245-
var ctorOpt = deserData.classInfo.constructors().stream()
246-
.filter(ctor -> Modifier.isPublic(ctor.flags()) && ctor.hasAnnotation(JsonCreator.class))
247-
.findFirst();
248-
249-
if (ctorOpt.isEmpty()) {
250-
if (deserData.classInfo.hasNoArgsConstructor() && !deserData.classInfo.isRecord()) {
251-
return deserData.methodCreator
252-
.newInstance(MethodDescriptor.ofConstructor(deserData.classInfo.name().toString()));
253-
}
254-
ctorOpt = deserData.classInfo.isRecord() ? Optional.of(deserData.classInfo.canonicalRecordConstructor())
255-
: deserData.classInfo.constructors().stream().filter(ctor -> Modifier.isPublic(ctor.flags())).findFirst();
256-
if (ctorOpt.isEmpty()) {
257-
return null;
258-
}
259-
}
260-
261-
MethodInfo ctor = ctorOpt.get();
262-
ResultHandle[] params = new ResultHandle[ctor.parameters().size()];
254+
ResultHandle[] params = new ResultHandle[deserData.constructor.parameters().size()];
263255
int i = 0;
264-
for (MethodParameterInfo paramInfo : ctor.parameters()) {
256+
for (MethodParameterInfo paramInfo : deserData.constructor.parameters()) {
265257
FieldSpecs fieldSpecs = fieldSpecsFromFieldParam(paramInfo);
266258
deserData.constructorFields.add(fieldSpecs.jsonName);
267259
ResultHandle fieldValue = deserData.methodCreator.invokeVirtualMethod(
@@ -271,7 +263,7 @@ private ResultHandle createDeserializedObject(DeserializationData deserData) {
271263
params[i++] = readValueFromJson(deserData.classCreator, deserData.methodCreator,
272264
deserData.methodCreator.getMethodParam(1), fieldSpecs, deserData.typeParametersIndex, fieldValue);
273265
}
274-
return deserData.methodCreator.newInstance(ctor, params);
266+
return deserData.methodCreator.newInstance(deserData.constructor, params);
275267
}
276268

277269
private boolean deserializeObjectFields(DeserializationData deserData, ResultHandle objHandle) {
@@ -344,7 +336,8 @@ private boolean deserializeFields(DeserializationData deserData, ResultHandle de
344336

345337
for (FieldInfo fieldInfo : classFields(deserData.classInfo)) {
346338
if (!deserializeFieldSpecs(deserData, deserializationContext, objHandle, fieldValue,
347-
deserializedFields, strSwitch, fieldSpecsFromField(deserData.classInfo, fieldInfo), valid))
339+
deserializedFields, strSwitch, fieldSpecsFromField(deserData.classInfo, deserData.constructor, fieldInfo),
340+
valid))
348341
return false;
349342
}
350343

@@ -529,7 +522,8 @@ protected boolean shouldGenerateCodeFor(ClassInfo classInfo) {
529522
return super.shouldGenerateCodeFor(classInfo) && classInfo.hasNoArgsConstructor();
530523
}
531524

532-
private record DeserializationData(ClassInfo classInfo, ClassCreator classCreator, MethodCreator methodCreator,
525+
private record DeserializationData(ClassInfo classInfo, MethodInfo constructor, ClassCreator classCreator,
526+
MethodCreator methodCreator,
533527
ResultHandle jsonNode, Map<String, Integer> typeParametersIndex, Set<String> constructorFields) {
534528
}
535529
}

extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,10 @@ private boolean serializeObjectData(ClassInfo classInfo, ClassCreator classCreat
220220

221221
private boolean serializeFields(ClassInfo classInfo, ClassCreator classCreator, MethodCreator serialize,
222222
SerializationContext ctx, Set<String> serializedFields) {
223+
MethodInfo constructor = findConstructor(classInfo).orElse(null);
224+
223225
for (FieldInfo fieldInfo : classFields(classInfo)) {
224-
FieldSpecs fieldSpecs = fieldSpecsFromField(classInfo, fieldInfo);
226+
FieldSpecs fieldSpecs = fieldSpecsFromField(classInfo, constructor, fieldInfo);
225227
if (fieldSpecs != null && serializedFields.add(fieldSpecs.jsonName)) {
226228
if (fieldSpecs.isIgnoredField()) {
227229
continue;

extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractSimpleJsonTest.java

+16
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,22 @@ public void testRecordWithEmptyConstructorEcho() {
742742
.body("age", Matchers.is(5));
743743
}
744744

745+
@Test
746+
public void testKotlinDataEcho() {
747+
RestAssured
748+
.with()
749+
.body("{\"access_token\":\"ABC\",\"expires_in\":3600}")
750+
.contentType("application/json; charset=utf-8")
751+
.post("/simple/kotlin-data-echo")
752+
.then()
753+
.statusCode(200)
754+
.contentType("application/json")
755+
.body("access_token", Matchers.is("ABC"))
756+
.body("expires_in", Matchers.is(3600))
757+
.extract()
758+
.asString();
759+
}
760+
745761
@Test
746762
public void testNullMapEcho() {
747763
RestAssured

extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java

+7
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ public DogRecord emptyCtorEchoRecord(DogRecord dogRecord) {
137137
return dogRecord;
138138
}
139139

140+
@POST
141+
@Path("/kotlin-data-echo")
142+
@Consumes(MediaType.APPLICATION_JSON)
143+
public TokenResponse echoKotlinData(TokenResponse tokenResponse) {
144+
return tokenResponse;
145+
}
146+
140147
@POST
141148
@Path("/null-map-echo")
142149
@Consumes(MediaType.APPLICATION_JSON)

extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public JavaArchive get() {
2727
Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class,
2828
NestedInterface.class, StateRecord.class, MapWrapper.class, GenericWrapper.class,
2929
Fruit.class, Price.class, DogRecord.class, ItemExtended.class, Book.class, LombokBook.class,
30-
PrimitiveTypesBean.class, PrimitiveTypesRecord.class)
30+
PrimitiveTypesBean.class, PrimitiveTypesRecord.class, TokenResponse.class)
3131
.addAsResource(new StringAsset("admin-expression=admin\n" +
3232
"user-expression=user\n" +
3333
"birth-date-roles=alice,bob\n"), "application.properties");

extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public JavaArchive get() {
2929
Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class,
3030
NestedInterface.class, StateRecord.class, MapWrapper.class, GenericWrapper.class,
3131
Fruit.class, Price.class, DogRecord.class, ItemExtended.class, Book.class, LombokBook.class,
32-
PrimitiveTypesBean.class, PrimitiveTypesRecord.class)
32+
PrimitiveTypesBean.class, PrimitiveTypesRecord.class, TokenResponse.class)
3333
.addAsResource(new StringAsset("admin-expression=admin\n" +
3434
"user-expression=user\n" +
3535
"birth-date-roles=alice,bob\n" +
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.quarkus.resteasy.reactive.jackson.deployment.test;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
public class TokenResponse {
6+
7+
private final String accessToken;
8+
private final Integer expiresIn;
9+
10+
public TokenResponse(@JsonProperty("access_token") String accessToken, @JsonProperty("expires_in") Integer expiresIn) {
11+
this.accessToken = accessToken;
12+
this.expiresIn = expiresIn;
13+
}
14+
15+
public String getAccessToken() {
16+
return accessToken;
17+
}
18+
19+
public Integer getExpiresIn() {
20+
return expiresIn;
21+
}
22+
}

0 commit comments

Comments
 (0)