diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java
index 79a1fc25..c5b7cba6 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java
@@ -163,9 +163,7 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
case JsonTokenId.ID_STRING:
return _fromString(parser, context, parser.getText());
case JsonTokenId.ID_EMBEDDED_OBJECT:
- // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
- // values quite easily
- return (Duration) parser.getEmbeddedObject();
+ return _fromEmbedded(parser, context);
case JsonTokenId.ID_START_ARRAY:
return _deserializeFromArray(parser, context);
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java
index ee50ac63..c48fa038 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java
@@ -344,7 +344,6 @@ protected JSR310DateTimeDeserializerBase> _withFormatOverrides(Deserialization
return deser;
}
- @SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
@@ -359,9 +358,7 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I
case JsonTokenId.ID_STRING:
return _fromString(parser, context, parser.getText());
case JsonTokenId.ID_EMBEDDED_OBJECT:
- // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
- // values quite easily
- return (T) parser.getEmbeddedObject();
+ return _fromEmbedded(parser, context);
case JsonTokenId.ID_START_ARRAY:
return _deserializeFromArray(parser, context);
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java
index b48b9225..80aafe8e 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java
@@ -149,6 +149,39 @@ protected boolean _isValidTimestampString(String str) {
return _isIntNumber(str) && NumberInput.inLongRange(str, (str.charAt(0) == '-'));
}
+ /**
+ * Helper method called to deserialize value from an "embedded object"
+ * (see {@link JsonToken#VALUE_EMBEDDED_OBJECT}): something binary formats
+ * (CBOR, Smile, Ion) and token buffering may expose.
+ *
+ * Since there is no guarantee that the embedded value actually is of the
+ * expected type -- and since generic containers (like
+ * {@code Map}) perform no runtime check of their own, due
+ * to type erasure -- value is verified to be compatible with the type this
+ * deserializer handles; incompatible value is passed to
+ * {@link DeserializationContext#handleUnexpectedToken} for possible recovery,
+ * or, failing that, for reporting as
+ * {@link com.fasterxml.jackson.databind.exc.MismatchedInputException}.
+ *
+ * @since 2.23
+ */
+ @SuppressWarnings("unchecked")
+ protected T _fromEmbedded(JsonParser p, DeserializationContext ctxt)
+ throws IOException
+ {
+ // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
+ // values quite easily
+ Object value = p.getEmbeddedObject();
+ if (value == null) {
+ return getNullValue(ctxt);
+ }
+ // 21-Sep-2026, tatu: [modules-java8#389] Must verify type compatibility
+ if (_valueClass.isInstance(value)) {
+ return (T) value;
+ }
+ return (T) ctxt.handleUnexpectedToken(_valueClass, p);
+ }
+
protected BOGUS _reportWrongToken(DeserializationContext context,
JsonToken exp, String unit) throws IOException
{
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java
index 96596a34..a3187454 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java
@@ -129,9 +129,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
}
// fall through
} else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
- // values quite easily
- return p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
} else if (p.isExpectedStartArrayToken()) {
return _deserializeFromArray(p, ctxt);
}
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java
index 5775a1d4..9b4da1d0 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java
@@ -168,7 +168,7 @@ public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws I
t);
}
if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- return (LocalDate) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
}
// 06-Jan-2018, tatu: Is this actually safe? Do users expect such coercion?
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java
index c76fdb0d..de0a1c69 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java
@@ -210,7 +210,7 @@ public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) thro
t);
}
if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- return (LocalDateTime) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
}
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
_throwNoNumericTimestampNeedTimeZone(p, ctxt);
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java
index f023be9a..3aa697da 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java
@@ -164,7 +164,7 @@ public LocalTime deserialize(JsonParser p, DeserializationContext ctxt) throws I
t);
}
if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- return (LocalTime) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
}
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
_throwNoNumericTimestampNeedTimeZone(p, ctxt);
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java
index 88d71697..7ace4c0b 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java
@@ -105,7 +105,7 @@ public MonthDay deserialize(JsonParser p, DeserializationContext ctxt) throws IO
}
return MonthDay.of(month, day);
} else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- return (MonthDay) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
}
return _handleUnexpectedToken(ctxt, p,
JsonToken.VALUE_STRING, JsonToken.START_ARRAY);
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java
index 0a23ac39..d214ed6b 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java
@@ -115,7 +115,7 @@ public OffsetTime deserialize(JsonParser p, DeserializationContext ctxt) throws
}
if (!p.isExpectedStartArrayToken()) {
if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- return (OffsetTime) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
}
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
_throwNoNumericTimestampNeedTimeZone(p, ctxt);
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java
index 7e6c3603..787a8379 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java
@@ -96,7 +96,7 @@ public Year deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce
} else if (t == JsonToken.VALUE_NUMBER_INT) {
return _fromNumber(ctxt, p.getIntValue());
} else if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
- return (Year) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
} else if (p.isExpectedStartArrayToken()){
return _deserializeFromArray(p, ctxt);
}
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java
index 4114343e..0a35ea51 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java
@@ -124,7 +124,7 @@ public YearMonth deserialize(JsonParser p, DeserializationContext ctxt) throws I
}
return YearMonth.of(year, month);
} else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
- return (YearMonth) p.getEmbeddedObject();
+ return _fromEmbedded(p, ctxt);
}
return _handleUnexpectedToken(ctxt, p,
JsonToken.VALUE_STRING, JsonToken.START_ARRAY);
diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java
new file mode 100644
index 00000000..19bec7e4
--- /dev/null
+++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java
@@ -0,0 +1,131 @@
+package com.fasterxml.jackson.datatype.jsr310.deser;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.MonthDay;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Period;
+import java.time.Year;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import com.fasterxml.jackson.databind.util.TokenBuffer;
+import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for [modules-java8#389]: "embedded object" values (as exposed by binary
+ * formats like CBOR/Smile, and by token buffering) must be verified to be
+ * compatible with the requested type; otherwise generic containers (like
+ * {@code Map}) would silently end up with wrong-typed values,
+ * due to type erasure.
+ */
+public class EmbeddedObjectDeser389Test extends ModuleTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+
+ static Stream supportedTypes()
+ {
+ return Stream.of(
+ // Types handled by `JSR310StringParsableDeserializer`
+ Arguments.of(Period.class, Period.ofDays(1), Duration.ofSeconds(1)),
+ Arguments.of(ZoneId.class, ZoneId.of("Europe/Helsinki"), Period.ofDays(1)),
+ // NOTE: `ZoneOffset` IS-A `ZoneId`, but not vice versa
+ Arguments.of(ZoneOffset.class, ZoneOffset.ofHours(2), ZoneId.of("Europe/Helsinki")),
+
+ // Types handled by `InstantDeserializer`: note that all are `Temporal`s
+ // so an unchecked cast would NOT catch these
+ Arguments.of(Instant.class, Instant.ofEpochSecond(1), LocalDate.of(2020, 1, 1)),
+ Arguments.of(OffsetDateTime.class,
+ OffsetDateTime.parse("2020-01-01T00:00:00+02:00"), Instant.ofEpochSecond(1)),
+ Arguments.of(ZonedDateTime.class,
+ ZonedDateTime.parse("2020-01-01T00:00:00+02:00"), Instant.ofEpochSecond(1)),
+
+ // ... and the rest of `java.time` types with embedded-object support
+ Arguments.of(Duration.class, Duration.ofSeconds(1), Period.ofDays(1)),
+ Arguments.of(LocalDate.class, LocalDate.of(2020, 1, 1), Instant.ofEpochSecond(1)),
+ Arguments.of(LocalDateTime.class,
+ LocalDateTime.of(2020, 1, 1, 12, 0), LocalDate.of(2020, 1, 1)),
+ Arguments.of(LocalTime.class, LocalTime.of(12, 0), LocalDate.of(2020, 1, 1)),
+ Arguments.of(OffsetTime.class,
+ OffsetTime.parse("12:00:00+02:00"), LocalTime.of(12, 0)),
+ Arguments.of(Year.class, Year.of(2020), YearMonth.of(2020, 1)),
+ Arguments.of(YearMonth.class, YearMonth.of(2020, 1), Year.of(2020)),
+ Arguments.of(MonthDay.class, MonthDay.of(1, 1), YearMonth.of(2020, 1))
+ );
+ }
+
+ // [modules-java8#389]
+ @ParameterizedTest
+ @MethodSource("supportedTypes")
+ public void testAcceptsCompatibleEmbeddedObject(Class> targetType,
+ Object validValue, Object incompatibleValue) throws Exception
+ {
+ assertSame(validValue, readEmbeddedValue(targetType, validValue));
+ }
+
+ // [modules-java8#389]
+ @ParameterizedTest
+ @MethodSource("supportedTypes")
+ public void testRejectsIncompatibleEmbeddedObject(Class> targetType,
+ Object validValue, Object incompatibleValue) throws Exception
+ {
+ // First: a value of completely unrelated type (as per #389, a CBOR byte string)
+ MismatchedInputException e = assertThrows(MismatchedInputException.class,
+ () -> readEmbeddedValue(targetType, new byte[] { 1, 2, 3 }));
+ verifyException(e, targetType.getName());
+
+ // Second: a "near miss", that is, another `java.time` value; these would
+ // NOT be caught by an unchecked/erased cast
+ e = assertThrows(MismatchedInputException.class,
+ () -> readEmbeddedValue(targetType, incompatibleValue));
+ verifyException(e, targetType.getName());
+ }
+
+ // [modules-java8#389]
+ @ParameterizedTest
+ @MethodSource("supportedTypes")
+ public void testAllowsNullEmbeddedObject(Class> targetType,
+ Object validValue, Object incompatibleValue) throws Exception
+ {
+ assertNull(readEmbeddedValue(targetType, null));
+ }
+
+ private Object readEmbeddedValue(Class> targetType, Object embeddedValue)
+ throws IOException
+ {
+ try (TokenBuffer buffer = new TokenBuffer(MAPPER, false)) {
+ buffer.writeStartObject();
+ buffer.writeFieldName("value");
+ buffer.writeEmbeddedObject(embeddedValue);
+ buffer.writeEndObject();
+
+ JavaType mapType = MAPPER.getTypeFactory()
+ .constructMapType(Map.class, String.class, targetType);
+ try (JsonParser parser = buffer.asParser()) {
+ Map, ?> result = MAPPER.readValue(parser, mapType);
+ return result.get("value");
+ }
+ }
+ }
+}
diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index c8693d31..7742a8c1 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -243,3 +243,13 @@ Seonwoo Jung (@seonwooj0810)
* Contributed fix for #76: Missing milliseconds, when serializing Java 8 date-time,
if they are zeros
(2.23.0)
+
+Manqing Zhou (@manqingzhou)
+ * Reported #389: Validate embedded objects (`JsonToken.VALUE_EMBEDDED_OBJECT`)
+ against expected `java.time` type, instead of returning them as-is
+ (2.23.0)
+
+Lux Liang (@lux-liang)
+ * Contributed fix for #389: Validate embedded objects (`JsonToken.VALUE_EMBEDDED_OBJECT`)
+ against expected `java.time` type, instead of returning them as-is
+ (2.23.0)
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 559b4889..29948ce1 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -14,6 +14,10 @@ Modules:
`JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS`)
(reported by @rycler)
(fix contributed by Seonwoo J)
+#389: Validate embedded objects (`JsonToken.VALUE_EMBEDDED_OBJECT`) against
+ expected `java.time` type, instead of returning them as-is
+ (reported by @manqingzhou)
+ (fix contributed by @lux-liang)
2.22.3 (21-Sep-2026)
2.22.2 (16-Aug-2026)