From f1a87b205cb73ad42640f5b90b76d8bce4db260f Mon Sep 17 00:00:00 2001 From: mariofusco Date: Mon, 9 Sep 2024 19:35:55 +0200 Subject: [PATCH] Make generated Jackson serializers to work with null values of boxed types --- .../processor/JacksonSerializerFactory.java | 44 ++++++++++++------- .../deployment/test/SimpleJsonResource.java | 6 ++- .../deployment/test/SimpleJsonTest.java | 18 ++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java index 39b9c10e3fe8e..f4d5da050439d 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java @@ -305,31 +305,45 @@ private void writeField(ClassInfo classInfo, FieldSpecs fieldSpecs, BytecodeCrea ResultHandle serializerProvider, ResultHandle valueHandle) { String pkgName = classInfo.name().packagePrefixName().toString(); generatedFields.computeIfAbsent(pkgName, pkg -> new HashSet<>()).add(fieldSpecs.jsonName); - MethodDescriptor writeFieldName = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeFieldName", void.class, - SerializableString.class); - ResultHandle serStringHandle = bytecode.readStaticField( - FieldDescriptor.of(pkgName + "." + SER_STRINGS_CLASS_NAME, fieldSpecs.jsonName, - SerializedString.class.getName())); - bytecode.invokeVirtualMethod(writeFieldName, jsonGenerator, serStringHandle); ResultHandle arg = fieldSpecs.toValueReaderHandle(bytecode, valueHandle); 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); MethodDescriptor primitiveWriter = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, primitiveMethodName, "void", fieldSpecs.writtenType()); - bytecode.invokeVirtualMethod(primitiveWriter, jsonGenerator, arg); + primitiveBytecode.invokeVirtualMethod(primitiveWriter, jsonGenerator, arg); return; } registerTypeToBeGenerated(fieldSpecs.fieldType, typeName); + writeFieldName(fieldSpecs, bytecode, jsonGenerator, pkgName); MethodDescriptor writeMethod = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writePOJO", void.class, Object.class); bytecode.invokeVirtualMethod(writeMethod, jsonGenerator, arg); } + private static void writeFieldName(FieldSpecs fieldSpecs, BytecodeCreator bytecode, ResultHandle jsonGenerator, + String pkgName) { + MethodDescriptor writeFieldName = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeFieldName", void.class, + SerializableString.class); + ResultHandle serStringHandle = bytecode.readStaticField( + FieldDescriptor.of(pkgName + "." + SER_STRINGS_CLASS_NAME, fieldSpecs.jsonName, + SerializedString.class.getName())); + bytecode.invokeVirtualMethod(writeFieldName, jsonGenerator, serStringHandle); + } + + private boolean isBoxedPrimitive(String typeName) { + return "java.lang.Character".equals(typeName) || "java.lang.Short".equals(typeName) + || "java.lang.Integer".equals(typeName) || "java.lang.Long".equals(typeName) + || "java.lang.Float".equals(typeName) || "java.lang.Double".equals(typeName) + || "java.lang.Boolean".equals(typeName); + } + private void registerTypeToBeGenerated(Type fieldType, String typeName) { if (!isCollectionType(fieldType, typeName)) { registerTypeToBeGenerated(typeName); @@ -489,7 +503,7 @@ private MethodInfo getterMethodInfo(ClassInfo classInfo, FieldInfo fieldInfo) { if (namedAccessor != null) { return namedAccessor; } - String methodName = (isBooleanType(fieldInfo.type().name().toString()) ? "is" : "get") + ucFirst(fieldInfo.name()); + String methodName = (fieldInfo.type().name().toString().equals("boolean") ? "is" : "get") + ucFirst(fieldInfo.name()); return findMethod(classInfo, methodName); } @@ -503,10 +517,6 @@ private static String ucFirst(String name) { return name.substring(0, 1).toUpperCase() + name.substring(1); } - private static boolean isBooleanType(String type) { - return type.equals("boolean") || type.equals("java.lang.Boolean"); - } - private static boolean vetoedClassName(String className) { return className.startsWith("java.") || className.startsWith("jakarta.") || className.startsWith("io.vertx.core.json."); } @@ -579,9 +589,13 @@ private String fieldName() { private String fieldNameFromMethod(MethodInfo methodInfo) { String methodName = methodInfo.name(); - return isBooleanType(methodInfo.returnType().toString()) - ? methodName.substring(2, 3).toLowerCase() + methodName.substring(3) - : methodName.substring(3, 4).toLowerCase() + methodName.substring(4); + if (methodName.startsWith("is")) { + return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); + } + if (methodName.startsWith("get")) { + return methodName.substring(3, 4).toLowerCase() + methodName.substring(4); + } + return methodName; } boolean hasUnknownAnnotation() { diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 05235155aad78..05f12b8e9ebf4 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -470,7 +470,8 @@ public ObjectWriter apply(ObjectMapper objectMapper, Type type) { type = ((ParameterizedType) type).getActualTypeArguments()[0]; } if (!type.getTypeName().equals(Person.class.getName())) { - throw new IllegalArgumentException("Only Person type can be handled"); + throw new IllegalArgumentException( + "Type'" + type.getTypeName() + "' cannot be handled. Only 'Person' type is valid"); } return objectMapper.writer().without(JsonWriteFeature.QUOTE_FIELD_NAMES); } @@ -490,7 +491,8 @@ public ObjectReader apply(ObjectMapper objectMapper, Type type) { type = ((ParameterizedType) type).getActualTypeArguments()[0]; } if (!type.getTypeName().equals(Person.class.getName())) { - throw new IllegalArgumentException("Only Person type can be handled"); + throw new IllegalArgumentException( + "Type'" + type.getTypeName() + "' cannot be handled. Only 'Person' type is valid"); } return objectMapper.reader().with(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index b7b48b1d4b670..8aa2a009e8418 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -670,6 +670,24 @@ public void testEcho() { .body("publicName", Matchers.is("Leo")) .body("privateName", Matchers.nullValue()) .body("age", Matchers.is(5)) + .body("vaccinated", Matchers.is(true)) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.nullValue()); + } + + @Test + public void testEchoWithMissingPrimitive() { + RestAssured + .with() + .body("{\"publicName\":\"Leo\",\"veterinarian\":{\"name\":\"Dolittle\"},\"age\":5}") + .contentType("application/json; charset=utf-8") + .post("/simple/dog-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("publicName", Matchers.is("Leo")) + .body("privateName", Matchers.nullValue()) + .body("age", Matchers.is(5)) .body("veterinarian.name", Matchers.is("Dolittle")) .body("veterinarian.title", Matchers.nullValue()); }