Skip to content

Commit 6a8549a

Browse files
authored
Merge pull request #53668 from mariofusco/jsonview
Support @JSONVIEW, @jsonvalue, @JsonIgnoreProperties and @JsonAnySetter annotations in generated Jackson serializers
2 parents 15f5250 + f744afa commit 6a8549a

5 files changed

Lines changed: 238 additions & 56 deletions

File tree

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

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828
import org.jboss.logging.Logger;
2929

3030
import com.fasterxml.jackson.annotation.JsonAlias;
31+
import com.fasterxml.jackson.annotation.JsonAnySetter;
3132
import com.fasterxml.jackson.annotation.JsonCreator;
3233
import com.fasterxml.jackson.annotation.JsonIgnore;
34+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
3335
import com.fasterxml.jackson.annotation.JsonProperty;
36+
import com.fasterxml.jackson.annotation.JsonValue;
37+
import com.fasterxml.jackson.annotation.JsonView;
3438
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
3539
import com.fasterxml.jackson.databind.annotation.JsonNaming;
3640

@@ -311,6 +315,20 @@ protected PropertyNamingStrategy getNamingStrategy(ClassInfo classInfo) {
311315
}
312316
}
313317

318+
protected static Set<String> getIgnoredProperties(ClassInfo classInfo) {
319+
AnnotationInstance ann = classInfo.declaredAnnotation(JsonIgnoreProperties.class);
320+
if (ann == null || ann.value() == null) {
321+
return Set.of();
322+
}
323+
String[] names = ann.value().asStringArray();
324+
return names.length == 0 ? Set.of() : Set.of(names);
325+
}
326+
327+
protected static boolean shouldIgnoreUnknownProperties(ClassInfo classInfo) {
328+
AnnotationInstance ann = classInfo.declaredAnnotation(JsonIgnoreProperties.class);
329+
return ann != null && ann.value("ignoreUnknown") != null && ann.value("ignoreUnknown").asBoolean();
330+
}
331+
314332
protected FieldSpecs fieldSpecsFromField(ClassInfo classInfo, MethodInfo constructor, FieldInfo fieldInfo,
315333
PropertyNamingStrategy namingStrategy) {
316334
if (Modifier.isStatic(fieldInfo.flags())) {
@@ -457,17 +475,37 @@ boolean isIgnoredField() {
457475
return annotations.get(JsonIgnore.class.getName()) != null;
458476
}
459477

478+
private static final Set<String> SUPPORTED_JACKSON_ANNOTATIONS = Set.of(
479+
JsonProperty.class.getName(),
480+
JsonIgnore.class.getName(),
481+
JsonIgnoreProperties.class.getName(),
482+
JsonAnySetter.class.getName(),
483+
JsonCreator.class.getName(),
484+
JsonAlias.class.getName(),
485+
JsonNaming.class.getName(),
486+
JsonValue.class.getName(),
487+
JsonView.class.getName());
488+
460489
static boolean isUnknownAnnotation(String ann) {
461490
if (ann.startsWith("com.fasterxml.jackson.")) {
462-
return !ann.equals(JsonProperty.class.getName()) &&
463-
!ann.equals(JsonIgnore.class.getName()) &&
464-
!ann.equals(JsonCreator.class.getName()) &&
465-
!ann.equals(JsonAlias.class.getName()) &&
466-
!ann.equals(JsonNaming.class.getName());
491+
return !SUPPORTED_JACKSON_ANNOTATIONS.contains(ann);
467492
}
468493
return ann.startsWith("jakarta.persistence.");
469494
}
470495

496+
String[] viewClasses() {
497+
AnnotationInstance jsonView = annotations.get(JsonView.class.getName());
498+
if (jsonView == null || jsonView.value() == null) {
499+
return null;
500+
}
501+
Type[] types = jsonView.value().asClassArray();
502+
String[] classNames = new String[types.length];
503+
for (int i = 0; i < types.length; i++) {
504+
classNames[i] = types[i].name().toString();
505+
}
506+
return classNames;
507+
}
508+
471509
ResultHandle toValueWriterHandle(BytecodeCreator bytecode, ResultHandle valueHandle) {
472510
return switch (fieldType.name().toString()) {
473511
case "char", "java.lang.Character" -> bytecode.invokeVirtualMethod(

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

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jboss.jandex.TypeVariable;
2727
import org.jboss.jandex.VoidType;
2828

29+
import com.fasterxml.jackson.annotation.JsonAnySetter;
2930
import com.fasterxml.jackson.core.JacksonException;
3031
import com.fasterxml.jackson.core.JsonParser;
3132
import com.fasterxml.jackson.core.ObjectCodec;
@@ -259,9 +260,12 @@ protected boolean createSerializationMethod(ClassInfo classInfo, ClassCreator cl
259260
generateTranslatableFieldNamesStaticField(classCreator, translatableNames);
260261
reverseIndexHandle = buildReverseIndexHandle(deserialize, classCreator, strategyHandle);
261262
}
263+
ResultHandle activeViewHandle = deserialize.invokeVirtualMethod(
264+
ofMethod(DeserializationContext.class, "getActiveView", Class.class),
265+
deserialize.getMethodParam(1));
262266
DeserializationData deserData = new DeserializationData(classInfo, ctor, classCreator, deserialize,
263267
getJsonNode(deserialize), parseTypeParameters(classInfo, classCreator), new HashSet<>(),
264-
namingStrategy, strategyHandle, reverseIndexHandle);
268+
namingStrategy, strategyHandle, reverseIndexHandle, activeViewHandle);
265269

266270
ResultHandle deserializedHandle = ctor.parametersCount() == 0
267271
? deserData.methodCreator.newInstance(MethodDescriptor.ofConstructor(deserData.classInfo.name().toString()))
@@ -365,7 +369,6 @@ private static ResultHandle resolveLookupName(BytecodeCreator bytecode, FieldSpe
365369
}
366370

367371
private boolean deserializeObjectFields(DeserializationData deserData, ResultHandle objHandle) {
368-
369372
ResultHandle fieldsIterator = deserData.methodCreator
370373
.invokeVirtualMethod(ofMethod(JsonNode.class, "fields", Iterator.class), deserData.jsonNode);
371374
BytecodeCreator loopCreator = deserData.methodCreator.whileLoop(c -> iteratorHasNext(c, fieldsIterator)).block();
@@ -381,6 +384,9 @@ private boolean deserializeObjectFields(DeserializationData deserData, ResultHan
381384
// save constructor field names before deserializeFields modifies the set
382385
Set<String> ctorFields = Set.copyOf(deserData.constructorFields);
383386

387+
Set<String> ignoredProperties = getIgnoredProperties(deserData.classInfo);
388+
deserData.constructorFields.addAll(ignoredProperties);
389+
384390
ResultHandle deserializationContext = deserData.methodCreator.getMethodParam(1);
385391
boolean result = deserializeFields(deserData, deserializationContext, objHandle, fieldValue,
386392
deserData.constructorFields, strSwitch);
@@ -391,28 +397,61 @@ private boolean deserializeObjectFields(DeserializationData deserData, ResultHan
391397
});
392398
}
393399

394-
strSwitch.defaultCase(bytecode -> {
395-
ResultHandle failOnUnknown = bytecode.invokeVirtualMethod(
396-
ofMethod(DeserializationContext.class, "isEnabled", boolean.class, DeserializationFeature.class),
397-
deserializationContext,
398-
bytecode.readStaticField(FieldDescriptor.of(DeserializationFeature.class,
399-
"FAIL_ON_UNKNOWN_PROPERTIES", DeserializationFeature.class)));
400-
BytecodeCreator trueBranch = bytecode.ifTrue(failOnUnknown).trueBranch();
401-
ResultHandle message = trueBranch.invokeVirtualMethod(
402-
ofMethod(String.class, "concat", String.class, String.class),
403-
trueBranch.load("Unrecognized field \""),
404-
trueBranch.invokeVirtualMethod(
405-
ofMethod(String.class, "concat", String.class, String.class),
406-
trueBranch.checkCast(fieldName, String.class),
407-
trueBranch.load("\"")));
408-
ResultHandle exception = trueBranch.newInstance(
409-
MethodDescriptor.ofConstructor(JsonMappingException.class, String.class), message);
410-
trueBranch.throwException(exception);
411-
});
412-
400+
MethodInfo anySetterMethod = findAnySetterMethod(deserData.classInfo);
401+
handleUnknownFields(deserData, ignoredProperties, ctorFields, strSwitch, deserializationContext, fieldName,
402+
fieldValue, objHandle, anySetterMethod);
413403
return result;
414404
}
415405

406+
private static void handleUnknownFields(DeserializationData deserData, Set<String> ignoredProperties,
407+
Set<String> ctorFields, Switch.StringSwitch strSwitch, ResultHandle deserializationContext,
408+
ResultHandle fieldName, ResultHandle fieldValue, ResultHandle objHandle, MethodInfo anySetterMethod) {
409+
// add no-op cases for explicitly ignored properties
410+
for (String ignoredProp : ignoredProperties) {
411+
if (!ctorFields.contains(ignoredProp)) {
412+
strSwitch.caseOf(ignoredProp, bytecode -> {
413+
});
414+
}
415+
}
416+
417+
if (anySetterMethod != null) {
418+
strSwitch.defaultCase(bytecode -> {
419+
ResultHandle deserializedValue = bytecode.invokeVirtualMethod(
420+
ofMethod(DeserializationContext.class, "readTreeAsValue", Object.class, JsonNode.class, Class.class),
421+
deserializationContext, fieldValue,
422+
bytecode.loadClass(anySetterMethod.parameterType(1).name().toString()));
423+
ResultHandle castedFieldName = bytecode.checkCast(fieldName, String.class);
424+
if (anySetterMethod.declaringClass().isInterface()) {
425+
bytecode.invokeInterfaceMethod(anySetterMethod, objHandle, castedFieldName, deserializedValue);
426+
} else {
427+
bytecode.invokeVirtualMethod(anySetterMethod, objHandle, castedFieldName, deserializedValue);
428+
}
429+
});
430+
} else if (shouldIgnoreUnknownProperties(deserData.classInfo)) {
431+
strSwitch.defaultCase(bytecode -> {
432+
});
433+
} else {
434+
strSwitch.defaultCase(bytecode -> {
435+
ResultHandle failOnUnknown = bytecode.invokeVirtualMethod(
436+
ofMethod(DeserializationContext.class, "isEnabled", boolean.class, DeserializationFeature.class),
437+
deserializationContext,
438+
bytecode.readStaticField(FieldDescriptor.of(DeserializationFeature.class,
439+
"FAIL_ON_UNKNOWN_PROPERTIES", DeserializationFeature.class)));
440+
BytecodeCreator trueBranch = bytecode.ifTrue(failOnUnknown).trueBranch();
441+
ResultHandle message = trueBranch.invokeVirtualMethod(
442+
ofMethod(String.class, "concat", String.class, String.class),
443+
trueBranch.load("Unrecognized field \""),
444+
trueBranch.invokeVirtualMethod(
445+
ofMethod(String.class, "concat", String.class, String.class),
446+
trueBranch.checkCast(fieldName, String.class),
447+
trueBranch.load("\"")));
448+
ResultHandle exception = trueBranch.newInstance(
449+
MethodDescriptor.ofConstructor(JsonMappingException.class, String.class), message);
450+
trueBranch.throwException(exception);
451+
});
452+
}
453+
}
454+
416455
/**
417456
* Generates bytecode that builds a {@code Map<String, String>} reverse-name index once,
418457
* before the field iteration loop. The map is {@code null} when no strategy is configured,
@@ -572,6 +611,8 @@ private void deserializeFieldSpecs(DeserializationData deserData, ResultHandle d
572611
private boolean deserializeField(DeserializationData deserData, BytecodeCreator bytecode,
573612
ResultHandle objHandle, ResultHandle fieldValue, FieldSpecs fieldSpecs,
574613
ResultHandle deserializationContext) {
614+
bytecode = deserializeViewClasses(deserData, bytecode, fieldSpecs);
615+
575616
boolean isBasicType = JacksonSerializationUtils.isBasicJsonType(fieldSpecs.fieldType);
576617

577618
// For non-basic types (objects, collections, boxed primitives, etc.), wrap in try-catch
@@ -604,6 +645,34 @@ private boolean deserializeField(DeserializationData deserData, BytecodeCreator
604645
return true;
605646
}
606647

648+
private static BytecodeCreator deserializeViewClasses(DeserializationData deserData, BytecodeCreator bytecode,
649+
FieldSpecs fieldSpecs) {
650+
String[] viewClasses = fieldSpecs.viewClasses();
651+
if (viewClasses != null) {
652+
ResultHandle viewClassesArray = bytecode.newArray(Class.class, viewClasses.length);
653+
for (int i = 0; i < viewClasses.length; i++) {
654+
bytecode.writeArrayValue(viewClassesArray, i, bytecode.loadClass(viewClasses[i]));
655+
}
656+
MethodDescriptor isViewIncluded = ofMethod(JacksonMapperUtil.class, "isViewIncluded",
657+
boolean.class, Class.class, Class[].class);
658+
ResultHandle included = bytecode.invokeStaticMethod(isViewIncluded, deserData.activeViewHandle(),
659+
viewClassesArray);
660+
bytecode = bytecode.ifTrue(included).trueBranch();
661+
}
662+
return bytecode;
663+
}
664+
665+
private MethodInfo findAnySetterMethod(ClassInfo classInfo) {
666+
for (MethodInfo method : classMethods(classInfo)) {
667+
if (method.hasAnnotation(JsonAnySetter.class)
668+
&& method.parametersCount() == 2
669+
&& !Modifier.isStatic(method.flags())) {
670+
return method;
671+
}
672+
}
673+
return null;
674+
}
675+
607676
private FieldSpecs fieldSpecsFromMethod(MethodInfo methodInfo, PropertyNamingStrategy namingStrategy) {
608677
return isSetterMethod(methodInfo) ? new FieldSpecs(null, null, methodInfo, namingStrategy) : null;
609678
}
@@ -761,6 +830,7 @@ protected boolean shouldGenerateCodeFor(ClassInfo classInfo) {
761830
private record DeserializationData(ClassInfo classInfo, MethodInfo constructor, ClassCreator classCreator,
762831
MethodCreator methodCreator,
763832
ResultHandle jsonNode, Map<String, Integer> typeParametersIndex, Set<String> constructorFields,
764-
PropertyNamingStrategy namingStrategy, ResultHandle strategyHandle, ResultHandle reverseIndexHandle) {
833+
PropertyNamingStrategy namingStrategy, ResultHandle strategyHandle, ResultHandle reverseIndexHandle,
834+
ResultHandle activeViewHandle) {
765835
}
766836
}

0 commit comments

Comments
 (0)