From a39892cc317730f346b94b281f1c3cdd9f32b218 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 5 May 2020 17:01:54 -0700 Subject: [PATCH] Fix #2683 --- release-notes/VERSION-2.x | 2 + .../deser/BeanDeserializerFactory.java | 44 +++++++++++++++---- .../deser/impl/FailingDeserializer.java | 10 +++-- .../impl/UnsupportedTypeDeserializer.java | 39 ++++++++++++++++ .../databind/ser/BeanSerializerFactory.java | 28 ++++++++++-- .../databind/ser/impl/FailingSerializer.java | 20 ++------- .../ser/impl/UnsupportedTypeSerializer.java | 37 ++++++++++++++++ .../jackson/databind/util/ClassUtil.java | 7 +++ .../deser/jdk/DateJava8FallbacksTest.java | 43 ++++++++++++++++++ .../databind/jsontype/TestWithGenerics.java | 2 +- 10 files changed, 200 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateJava8FallbacksTest.java diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 43bc0894e5..f25401e095 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -8,6 +8,8 @@ Project: jackson-databind #792: Deserialization Not Working Right with Generic Types and Builders (reported by Mike G; fix contributed by Ville K) +#2683: Explicitly fail (de)serialization of `java.time.*` types in absence of + registered custom (de)serializers #2707: Improve description included in by `DeserializationContext.handleUnexpectedToken()` 2.11.1 (not yet released) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index f9eee85870..8b93b7fd1f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -93,7 +93,7 @@ public JsonDeserializer createBeanDeserializer(DeserializationContext ct throws JsonMappingException { final DeserializationConfig config = ctxt.getConfig(); - // We may also have custom overrides: + // First: we may also have custom overrides: JsonDeserializer deser = _findCustomBeanDeserializer(type, config, beanDesc); if (deser != null) { // [databind#2392] @@ -104,10 +104,8 @@ public JsonDeserializer createBeanDeserializer(DeserializationContext ct } return (JsonDeserializer) deser; } - /* One more thing to check: do we have an exception type - * (Throwable or its sub-classes)? If so, need slightly - * different handling. - */ + // One more thing to check: do we have an exception type (Throwable or its + // sub-classes)? If so, need slightly different handling. if (type.isThrowable()) { return buildThrowableDeserializer(ctxt, type, beanDesc); } @@ -139,6 +137,14 @@ public JsonDeserializer createBeanDeserializer(DeserializationContext ct } // For checks like [databind#1599] _validateSubType(ctxt, type, beanDesc); + + // 05-May-2020, tatu: [databind#2683] Let's actually pre-emptively catch + // certain types (for now, java.time.*) to give better error messages + deser = _findUnsupportedTypeDeserializer(ctxt, type, beanDesc); + if (deser != null) { + return (JsonDeserializer)deser; + } + // Use generic bean introspection to build deserializer return buildBeanDeserializer(ctxt, type, beanDesc); } @@ -181,7 +187,30 @@ protected JsonDeserializer findStdDeserializer(DeserializationContext ctxt, } return deser; } - + + /** + * Helper method called to see if given type, otherwise to be taken as POJO type, + * is "known but not supported" JDK type, and if so, return alternate handler + * (deserializer). + * Initially added to support more meaningful error messages when "Java 8 date/time" + * support module not registered. + * + * @since 2.12 + */ + protected JsonDeserializer _findUnsupportedTypeDeserializer(DeserializationContext ctxt, + JavaType type, BeanDescription beanDesc) + throws JsonMappingException + { + if (ClassUtil.isJava8TimeClass(type.getRawClass())) { + // 05-May-2020, tatu: Should we check for possible Shape override to "POJO"? + // (to let users force 'serialize-as-POJO'? + return new UnsupportedTypeDeserializer(type, +"Java 8 date/time type "+ClassUtil.getTypeDescription(type) ++" not supported by default: please register module `jackson-datatype-jsr310` to add handling"); + } + return null; + } + protected JavaType materializeAbstractType(DeserializationContext ctxt, JavaType type, BeanDescription beanDesc) throws JsonMappingException @@ -434,8 +463,7 @@ public JsonDeserializer buildThrowableDeserializer(DeserializationContex /* /********************************************************** - /* Helper methods for Bean deserializer construction, - /* overridable by sub-classes + /* Helper methods for Bean deserializer construction /********************************************************** */ diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java index 9df6742bd6..ba9b55299a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java @@ -4,12 +4,11 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; /** * Special bogus "serializer" that will throw - * {@link JsonMappingException} if an attempt is made to deserialize + * {@link com.fasterxml.jackson.databind.exc.MismatchedInputException} if an attempt is made to deserialize * a value. This is used as placeholder to avoid NPEs for uninitialized * structured serializers or handlers. */ @@ -20,7 +19,12 @@ public class FailingDeserializer extends StdDeserializer protected final String _message; public FailingDeserializer(String m) { - super(Object.class); + this(Object.class, m); + } + + // @since 2.12 + public FailingDeserializer(Class rawType, String m) { + super(rawType); _message = m; } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java new file mode 100644 index 0000000000..a7a39148b0 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java @@ -0,0 +1,39 @@ +package com.fasterxml.jackson.databind.deser.impl; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +/** + * Special bogus "serializer" that will throw + * {@link com.fasterxml.jackson.databind.exc.MismatchedInputException} + * if an attempt is made to deserialize a value. + * This is used for "known unknown" types: types that we can recognize + * but can not support easily (or support known to be added via extension + * module). + * + * @since 2.12 + */ +public class UnsupportedTypeDeserializer extends StdDeserializer +{ + private static final long serialVersionUID = 1L; + + protected final JavaType _type; + + protected final String _message; + + public UnsupportedTypeDeserializer(JavaType t, String m) { + super(t); + _type = t; + _message = m; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + ctxt.reportBadDefinition(_type, _message); + return null; + } +} 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 79c9a96f03..66e682e5fb 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; +import com.fasterxml.jackson.databind.deser.impl.UnsupportedTypeDeserializer; import com.fasterxml.jackson.databind.introspect.*; import com.fasterxml.jackson.databind.jsontype.NamedType; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; @@ -16,6 +17,7 @@ import com.fasterxml.jackson.databind.ser.impl.FilteredBeanPropertyWriter; import com.fasterxml.jackson.databind.ser.impl.ObjectIdWriter; import com.fasterxml.jackson.databind.ser.impl.PropertyBasedObjectIdGenerator; +import com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer; import com.fasterxml.jackson.databind.ser.std.MapSerializer; import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer; import com.fasterxml.jackson.databind.type.ReferenceType; @@ -376,6 +378,12 @@ protected JsonSerializer constructBeanOrAddOnSerializer(SerializerProvid return prov.getUnknownTypeSerializer(Object.class); // throw new IllegalArgumentException("Cannot create bean serializer for Object.class"); } + + JsonSerializer ser = _findUnsupportedTypeSerializer(prov, type, beanDesc); + if (ser != null) { + return (JsonSerializer) ser; + } + final SerializationConfig config = prov.getConfig(); BeanSerializerBuilder builder = constructBeanSerializerBuilder(beanDesc); builder.setConfig(config); @@ -447,9 +455,8 @@ protected JsonSerializer constructBeanOrAddOnSerializer(SerializerProvid } } - JsonSerializer ser = null; try { - ser = (JsonSerializer) builder.build(); + ser = builder.build(); } catch (RuntimeException e) { return prov.reportBadTypeDefinition(beanDesc, "Failed to construct BeanSerializer for %s: (%s) %s", beanDesc.getType(), e.getClass().getName(), e.getMessage()); @@ -467,7 +474,7 @@ protected JsonSerializer constructBeanOrAddOnSerializer(SerializerProvid } } } - return ser; + return (JsonSerializer) ser; } protected ObjectIdWriter constructObjectIdHandler(SerializerProvider prov, @@ -814,4 +821,19 @@ protected BeanPropertyWriter _constructWriter(SerializerProvider prov, return pb.buildWriter(prov, propDef, type, annotatedSerializer, typeSer, contentTypeSer, accessor, staticTyping); } + + protected JsonSerializer _findUnsupportedTypeSerializer(SerializerProvider ctxt, + JavaType type, BeanDescription beanDesc) + throws JsonMappingException + { + if (ClassUtil.isJava8TimeClass(type.getRawClass())) { + // 05-May-2020, tatu: Should we check for possible Shape override to "POJO"? + // (to let users force 'serialize-as-POJO'? + return new UnsupportedTypeSerializer(type, +"Java 8 date/time type "+ClassUtil.getTypeDescription(type) ++" not supported by default: please register module `jackson-datatype-jsr310` to add handling"); + } + return null; + + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java index 6021fa89e0..e047c60108 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java @@ -1,14 +1,11 @@ package com.fasterxml.jackson.databind.ser.impl; import java.io.IOException; -import java.lang.reflect.Type; import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.JavaType; + import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.ser.std.StdSerializer; /** @@ -30,19 +27,8 @@ public FailingSerializer(String msg) { } @Override - public void serialize(Object value, JsonGenerator g, SerializerProvider provider) throws IOException - { - provider.reportMappingProblem(_msg); - } - - @Override - public JsonNode getSchema(SerializerProvider provider, Type typeHint) throws JsonMappingException { - return null; - } - - @Override - public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + public void serialize(Object value, JsonGenerator g, SerializerProvider ctxt) throws IOException { - ; + ctxt.reportMappingProblem(_msg); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java new file mode 100644 index 0000000000..b849ee3d2d --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.databind.ser.impl; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +/** + * Special bogus "serializer" that will throw + * {@link com.fasterxml.jackson.databind.exc.InvalidDefinitionException} if its {@link #serialize} + * gets invoked. Most commonly registered as handler for unknown types, + * as well as for catching unintended usage (like trying to use null + * as Map/Object key). + */ +public class UnsupportedTypeSerializer + extends StdSerializer +{ + private static final long serialVersionUID = 1L; + + protected final JavaType _type; + + protected final String _message; + + public UnsupportedTypeSerializer(JavaType t, String msg) { + super(Object.class); + _type = t; + _message = msg; + } + + @Override + public void serialize(Object value, JsonGenerator g, SerializerProvider ctxt) throws IOException { + ctxt.reportBadDefinition(_type, _message); + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java index 2a984dc903..e3fb8caad7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java @@ -1084,6 +1084,13 @@ public static boolean isJDKClass(Class rawType) { return rawType.getName().startsWith("java."); } + /** + * @since 2.12 + */ + public static boolean isJava8TimeClass(Class rawType) { + return rawType.getName().startsWith("java.time."); + } + /* /********************************************************** /* Access to various Class definition aspects; possibly diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateJava8FallbacksTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateJava8FallbacksTest.java new file mode 100644 index 0000000000..41d9fa4f0f --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateJava8FallbacksTest.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.databind.deser.jdk; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; + +// [databind#2683]: add fallback handling for Java 8 date/time types, to +// prevent accidental serialzization as POJOs, as well as give more information +// on deserialization attempts +public class DateJava8FallbacksTest extends BaseMapTest +{ + private final ObjectMapper MAPPER = newJsonMapper(); + + private final OffsetDateTime DATETIME_EPOCH = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), + ZoneOffset.of("Z")); + + // Test to prevent serialization as POJO, without Java 8 date/time module: + public void testPreventSerialization() throws Exception + { + try { + String json = MAPPER.writerWithDefaultPrettyPrinter() + .writeValueAsString(DATETIME_EPOCH); + fail("Should not pass, wrote out as\n: "+json); + } catch (InvalidDefinitionException e) { + verifyException(e, "Java 8 date/time type `java.time.OffsetDateTime` not supported by default"); + verifyException(e, "please register module `jackson-datatype-jsr310`"); + } + } + + public void testBetterDeserializationError() throws Exception + { + try { + OffsetDateTime result = MAPPER.readValue(" 0 ", OffsetDateTime.class); + fail("Not expecting to pass, resulted in: "+result); + } catch (InvalidDefinitionException e) { + verifyException(e, "Java 8 date/time type `java.time.OffsetDateTime` not supported by default"); + verifyException(e, "please register module `jackson-datatype-jsr310`"); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java index 15d95f82d1..8a066f6d24 100644 --- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java @@ -126,7 +126,7 @@ public ContainerWithTwoAnimals(U a1, V a2) { otherAnimal = a2; } } - + /* /********************************************************** /* Unit tests