diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java index 87b78a20a6..4e9e963409 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java @@ -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)"); } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/DefaultAccessorNamingStrategy.java b/src/main/java/com/fasterxml/jackson/databind/introspect/DefaultAccessorNamingStrategy.java index 4f495950d8..c25fedfd8e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/DefaultAccessorNamingStrategy.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/DefaultAccessorNamingStrategy.java @@ -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; @@ -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 diff --git a/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java b/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java index f8b6e7ad70..604e05be60 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java +++ b/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java @@ -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 @@ -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 { @@ -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; @@ -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 { @@ -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 names) @@ -205,6 +224,11 @@ public AnnotatedConstructor locate(List 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) { diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java index 1185d4aa99..75e884cb8c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java @@ -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 @@ -476,7 +477,10 @@ protected JsonSerializer 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(); } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnknownSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnknownSerializer.java index d1f167c6a7..16c9c86451 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnknownSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnknownSerializer.java @@ -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 @@ -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())); + } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/util/NativeImageUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/NativeImageUtil.java new file mode 100644 index 0000000000..7fb5f813a3 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/util/NativeImageUtil.java @@ -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; + } +}