Skip to content

Commit

Permalink
Allow (de)serializing records using Bean(De)SerializerModifier even…
Browse files Browse the repository at this point in the history
… when reflection is unavailable (#3417)
  • Loading branch information
yawkat authored May 1, 2022
1 parent e073287 commit 2abd8b9
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,10 @@ protected Object deserializeFromObjectUsingNonDefault(JsonParser p,
return ctxt.handleMissingInstantiator(raw, null, p,
"non-static inner classes like this can only by instantiated using default, no-argument constructor");
}
if (NativeImageUtil.needsReflectionConfiguration(raw)) {
return ctxt.handleMissingInstantiator(raw, null, p,
"cannot deserialize from Object value (no delegate- or property-based Creator): this appears to be a native image, in which case you may need to configure reflection for the class that is to be deserialized");
}
return ctxt.handleMissingInstantiator(raw, getValueInstantiator(), p,
"cannot deserialize from Object value (no delegate- or property-based Creator)");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.fasterxml.jackson.databind.introspect;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

Expand Down Expand Up @@ -527,10 +529,10 @@ public RecordNaming(MapperConfig<?> config, AnnotatedClass forClass) {
// trickier: regular fields are ok (handled differently), but should
// we also allow getter discovery? For now let's do so
"get", "is", null);
_fieldNames = new HashSet<>();
for (String name : JDK14Util.getRecordFieldNames(forClass.getRawType())) {
_fieldNames.add(name);
}
String[] recordFieldNames = JDK14Util.getRecordFieldNames(forClass.getRawType());
_fieldNames = recordFieldNames == null ?
Collections.emptySet() :
new HashSet<>(Arrays.asList(recordFieldNames));
}

@Override
Expand Down
76 changes: 50 additions & 26 deletions src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.NativeImageUtil;

/**
* Helper class to support some of JDK 14 (and later) features
Expand Down Expand Up @@ -76,6 +77,10 @@ public static RecordAccessor instance() {
public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentException
{
final Object[] components = recordComponents(recordType);
if (components == null) {
// not a record, or no reflective access on native image
return null;
}
final String[] names = new String[components.length];
for (int i = 0; i < components.length; i++) {
try {
Expand All @@ -92,6 +97,10 @@ public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentE
public RawTypeName[] getRecordFields(Class<?> recordType) throws IllegalArgumentException
{
final Object[] components = recordComponents(recordType);
if (components == null) {
// not a record, or no reflective access on native image
return null;
}
final RawTypeName[] results = new RawTypeName[components.length];
for (int i = 0; i < components.length; i++) {
String name;
Expand Down Expand Up @@ -120,10 +129,14 @@ protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentE
try {
return (Object[]) RECORD_GET_RECORD_COMPONENTS.invoke(recordType);
} catch (Exception e) {
if (NativeImageUtil.isUnsupportedFeatureError(e)) {
return null;
}
throw new IllegalArgumentException("Failed to access RecordComponents of type "
+ClassUtil.nameOf(recordType));
}
}

}

static class RawTypeName {
Expand Down Expand Up @@ -153,37 +166,43 @@ static class CreatorLocator {
_config = ctxt.getConfig();

_recordFields = RecordAccessor.instance().getRecordFields(beanDesc.getBeanClass());
final int argCount = _recordFields.length;

// And then locate the canonical constructor; must be found, if not, fail
// altogether (so we can figure out what went wrong)
AnnotatedConstructor primary = null;

// One special case: empty Records, empty constructor is separate case
if (argCount == 0) {
primary = beanDesc.findDefaultConstructor();
_constructors = Collections.singletonList(primary);
} else {
if (_recordFields == null) {
// not a record, or no reflective access on native image
_constructors = beanDesc.getConstructors();
main_loop:
for (AnnotatedConstructor ctor : _constructors) {
if (ctor.getParameterCount() != argCount) {
continue;
}
for (int i = 0; i < argCount; ++i) {
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
continue main_loop;
_primaryConstructor = null;
} else {
final int argCount = _recordFields.length;

// And then locate the canonical constructor; must be found, if not, fail
// altogether (so we can figure out what went wrong)
AnnotatedConstructor primary = null;

// One special case: empty Records, empty constructor is separate case
if (argCount == 0) {
primary = beanDesc.findDefaultConstructor();
_constructors = Collections.singletonList(primary);
} else {
_constructors = beanDesc.getConstructors();
main_loop:
for (AnnotatedConstructor ctor : _constructors) {
if (ctor.getParameterCount() != argCount) {
continue;
}
for (int i = 0; i < argCount; ++i) {
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
continue main_loop;
}
}
primary = ctor;
break;
}
primary = ctor;
break;
}
if (primary == null) {
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
+ClassUtil.getTypeDescription(_beanDesc.getType()));
}
_primaryConstructor = primary;
}
if (primary == null) {
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
+ClassUtil.getTypeDescription(_beanDesc.getType()));
}
_primaryConstructor = primary;
}

public AnnotatedConstructor locate(List<String> names)
Expand All @@ -205,6 +224,11 @@ public AnnotatedConstructor locate(List<String> names)
}
}

if (_recordFields == null) {
// not a record, or no reflective access on native image
return null;
}

// By now we have established that the canonical constructor is the one to use
// and just need to gather implicit names to return
for (RawTypeName field : _recordFields) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.Converter;
import com.fasterxml.jackson.databind.util.IgnorePropertiesUtil;
import com.fasterxml.jackson.databind.util.NativeImageUtil;

/**
* Factory class that can provide serializers for any regular Java beans
Expand Down Expand Up @@ -476,7 +477,10 @@ protected JsonSerializer<Object> constructBeanOrAddOnSerializer(SerializerProvid
}
if (ser == null) { // Means that no properties were found
// 21-Aug-2020, tatu: Empty Records should be fine tho
if (type.isRecordType()) {
// 18-Mar-2022, yawkat: Record will also appear empty when missing reflection info.
// needsReflectionConfiguration will check that a constructor is present, else we fall back to the empty
// bean error msg
if (type.isRecordType() && !NativeImageUtil.needsReflectionConfiguration(type.getRawClass())) {
return builder.createDummy();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.ToEmptyObjectSerializer;
import com.fasterxml.jackson.databind.util.NativeImageUtil;

@SuppressWarnings("serial")
public class UnknownSerializer
Expand Down Expand Up @@ -43,8 +44,15 @@ public void serializeWithType(Object value, JsonGenerator gen, SerializerProvide

protected void failForEmpty(SerializerProvider prov, Object value)
throws JsonMappingException {
prov.reportBadDefinition(handledType(), String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
value.getClass().getName()));
Class<?> cl = value.getClass();
if (NativeImageUtil.needsReflectionConfiguration(cl)) {
prov.reportBadDefinition(handledType(), String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS). This appears to be a native image, in which case you may need to configure reflection for the class that is to be serialized",
cl.getName()));
} else {
prov.reportBadDefinition(handledType(), String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
cl.getName()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.fasterxml.jackson.databind.util;

import java.lang.reflect.InvocationTargetException;

/**
* Utilities for graal native image support.
*/
public class NativeImageUtil {
private static final boolean RUNNING_IN_SVM;

static {
RUNNING_IN_SVM = System.getProperty("org.graalvm.nativeimage.imagecode") != null;
}

private NativeImageUtil() {
}

/**
* Check whether we're running in substratevm native image runtime mode. This check cannot be a constant, because
* the static initializer may run early during build time
*/
private static boolean isRunningInNativeImage() {
return RUNNING_IN_SVM && System.getProperty("org.graalvm.nativeimage.imagecode").equals("runtime");
}

/**
* Check whether the given error is a substratevm UnsupportedFeatureError
*/
public static boolean isUnsupportedFeatureError(Throwable e) {
if (!isRunningInNativeImage()) {
return false;
}
if (e instanceof InvocationTargetException) {
e = e.getCause();
}
return e.getClass().getName().equals("com.oracle.svm.core.jdk.UnsupportedFeatureError");
}

/**
* Check whether the given class is likely missing reflection configuration (running in native image, and no
* members visible in reflection).
*/
public static boolean needsReflectionConfiguration(Class<?> cl) {
if (!isRunningInNativeImage()) {
return false;
}
// records list their fields but not other members
return (cl.getDeclaredFields().length == 0 || ClassUtil.isRecordType(cl)) &&
cl.getDeclaredMethods().length == 0 &&
cl.getDeclaredConstructors().length == 0;
}
}

0 comments on commit 2abd8b9

Please sign in to comment.