diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java
index 36bebe27..19c684ee 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java
@@ -84,7 +84,29 @@ public enum JavaTimeFeature implements JacksonFeature
*
* @since 2.23
*/
- ALWAYS_WRITE_SUBSECOND_DIGITS(false)
+ ALWAYS_WRITE_SUBSECOND_DIGITS(false),
+
+ /**
+ * Feature that controls whether stringified numbers (JSON Strings that
+ * without quotes would be legal JSON Numbers) may be deserialized as
+ * {@link java.time.Duration} values (enabled) or not (disabled).
+ *
+ * When disabled (the default), JSON Strings are parsed with
+ * {@link java.time.Duration#parse} and must be ISO-8601 duration
+ * representations such as {@code "PT1H"} -- an int-like String such as
+ * {@code "3600"} fails.
+ * When enabled, integer and decimal numeric Strings are handled the same
+ * as JSON numbers: integers use {@link com.fasterxml.jackson.annotation.JsonFormat}
+ * pattern unit conversion (or
+ * {@link com.fasterxml.jackson.databind.DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}),
+ * and decimals are treated as seconds with fractional nanos.
+ * ISO-8601 duration Strings remain accepted either way.
+ *
+ * Default setting is disabled, for backwards compatibility.
+ *
+ * @since 2.23
+ */
+ ALLOW_STRINGIFIED_DURATION_VALUES(false)
;
/**
diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java
index 80ede099..75d8b7ea 100644
--- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java
+++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java
@@ -130,7 +130,7 @@ public void setupModule(SetupContext context) {
InstantDeserializer.ZONED_DATE_TIME.withFeatures(_features));
// // Other deserializers
- desers.addDeserializer(Duration.class, DurationDeserializer.INSTANCE);
+ desers.addDeserializer(Duration.class, DurationDeserializer.INSTANCE.withFeatures(_features));
desers.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE.withFeatures(_features));
desers.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE.withFeatures(_features));
desers.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
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..92270582 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
@@ -29,10 +29,12 @@
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.core.StreamReadCapability;
import com.fasterxml.jackson.core.io.NumberInput;
+import com.fasterxml.jackson.core.util.JacksonFeatureSet;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;
import com.fasterxml.jackson.datatype.jsr310.util.DurationUnitConverter;
/**
@@ -69,10 +71,21 @@ public class DurationDeserializer extends JSR310DeserializerBase
*/
protected final Boolean _readTimestampsAsNanosOverride;
+ /**
+ * Flag for {@link JavaTimeFeature#ALLOW_STRINGIFIED_DURATION_VALUES}
+ *
+ * @since 2.23
+ */
+ protected final boolean _allowStringifiedDurationValues;
+
+ private final static boolean DEFAULT_ALLOW_STRINGIFIED_DURATION_VALUES
+ = JavaTimeFeature.ALLOW_STRINGIFIED_DURATION_VALUES.enabledByDefault();
+
public DurationDeserializer() {
super(Duration.class);
_durationUnitConverter = null;
_readTimestampsAsNanosOverride = null;
+ _allowStringifiedDurationValues = DEFAULT_ALLOW_STRINGIFIED_DURATION_VALUES;
}
/**
@@ -82,6 +95,7 @@ protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
super(base, leniency);
_durationUnitConverter = base._durationUnitConverter;
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _allowStringifiedDurationValues = base._allowStringifiedDurationValues;
}
/**
@@ -91,6 +105,7 @@ protected DurationDeserializer(DurationDeserializer base, DurationUnitConverter
super(base, base._isLenient);
_durationUnitConverter = converter;
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _allowStringifiedDurationValues = base._allowStringifiedDurationValues;
}
/**
@@ -103,6 +118,19 @@ protected DurationDeserializer(DurationDeserializer base,
super(base, leniency);
_durationUnitConverter = converter;
_readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
+ _allowStringifiedDurationValues = base._allowStringifiedDurationValues;
+ }
+
+ /**
+ * @since 2.23
+ */
+ protected DurationDeserializer(DurationDeserializer base,
+ JacksonFeatureSet features)
+ {
+ super(base, base._isLenient);
+ _durationUnitConverter = base._durationUnitConverter;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _allowStringifiedDurationValues = features.isEnabled(JavaTimeFeature.ALLOW_STRINGIFIED_DURATION_VALUES);
}
@Override
@@ -114,6 +142,16 @@ protected DurationDeserializer withConverter(DurationUnitConverter converter) {
return new DurationDeserializer(this, converter);
}
+ /**
+ * @since 2.23
+ */
+ public DurationDeserializer withFeatures(JacksonFeatureSet features) {
+ if (_allowStringifiedDurationValues == features.isEnabled(JavaTimeFeature.ALLOW_STRINGIFIED_DURATION_VALUES)) {
+ return this;
+ }
+ return new DurationDeserializer(this, features);
+ }
+
@Override
public JsonDeserializer> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException
@@ -197,6 +235,26 @@ protected Duration _fromString(JsonParser parser, DeserializationContext ctxt,
&& _isValidTimestampString(value)) {
return _fromTimestamp(ctxt, NumberInput.parseLong(value));
}
+ // [modules-java8#232]: optionally accept JSON stringified numbers
+ if (_allowStringifiedDurationValues) {
+ int dots = _countPeriods(value);
+ if (dots >= 0) { // negative if not simple number
+ try {
+ if (dots == 0) {
+ _validateTimestampLength(parser, value, false);
+ return _fromTimestamp(ctxt, NumberInput.parseLong(value));
+ }
+ if (dots == 1) {
+ _validateTimestampLength(parser, value, true);
+ return DecimalUtils.extractSecondsAndNanos(
+ NumberInput.parseBigDecimal(value, false),
+ Duration::ofSeconds, false);
+ }
+ } catch (NumberFormatException e) {
+ // fall through to ISO-8601 handling, to get error there
+ }
+ }
+ }
try {
return Duration.parse(value);
@@ -205,6 +263,28 @@ && _isValidTimestampString(value)) {
}
}
+ // Helper to find Strings of form "all digits" and "digits.digits"
+ protected int _countPeriods(String str)
+ {
+ int commas = 0;
+ int i = 0;
+ int ch = str.charAt(i);
+ if (ch == '-') {
+ ++i;
+ }
+ for (int end = str.length(); i < end; ++i) {
+ ch = str.charAt(i);
+ if (ch < '0' || ch > '9') {
+ if (ch == '.') {
+ ++commas;
+ } else {
+ return -1;
+ }
+ }
+ }
+ return commas;
+ }
+
protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
if (_durationUnitConverter != null) {
return _durationUnitConverter.convert(ts);
diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeser232Test.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeser232Test.java
new file mode 100644
index 00000000..5baec3c2
--- /dev/null
+++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeser232Test.java
@@ -0,0 +1,112 @@
+package com.fasterxml.jackson.datatype.jsr310.deser;
+
+import java.time.Duration;
+import java.util.Locale;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for [modules-java8#232]: {@link JavaTimeFeature#ALLOW_STRINGIFIED_DURATION_VALUES}.
+ */
+public class DurationDeser232Test extends ModuleTestBase
+{
+ static class SecondsWrapper {
+ @JsonFormat(pattern = "SECONDS")
+ public Duration value;
+ }
+
+ // NOTE: cannot use `ModuleTestBase.mapperBuilder()` here, since it already registers a
+ // plain `JavaTimeModule` and duplicate registrations of the same module are ignored
+ private static JsonMapper.Builder builderWithFeature() {
+ return JsonMapper.builder()
+ .defaultLocale(Locale.ENGLISH)
+ .addModule(new JavaTimeModule()
+ .enable(JavaTimeFeature.ALLOW_STRINGIFIED_DURATION_VALUES));
+ }
+
+ private final ObjectMapper MAPPER = builderWithFeature().build();
+ private final ObjectReader READER = MAPPER.readerFor(Duration.class);
+ private final ObjectReader DEFAULT_READER = newMapper().readerFor(Duration.class);
+
+ @Test
+ public void testStringifiedIntegerFailsByDefault() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> DEFAULT_READER.readValue(q("3600")));
+ }
+
+ @Test
+ public void testIsoStringWorksByDefault() throws Exception
+ {
+ assertEquals(Duration.ofSeconds(3600L), DEFAULT_READER.readValue(q("PT3600S")));
+ }
+
+ @Test
+ public void testStringifiedIntegerWhenEnabled() throws Exception
+ {
+ assertEquals(Duration.ofSeconds(3600L), READER.readValue(q("3600")));
+ }
+
+ @Test
+ public void testStringifiedNegativeIntegerWhenEnabled() throws Exception
+ {
+ assertEquals(Duration.ofSeconds(-3600L), READER.readValue(q("-3600")));
+ }
+
+ @Test
+ public void testStringifiedIntegerMillisWhenNanosDisabled() throws Exception
+ {
+ Duration value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(q("60000"));
+ assertEquals(Duration.ofSeconds(60L), value);
+ }
+
+ @Test
+ public void testStringifiedDecimalWhenEnabled() throws Exception
+ {
+ assertEquals(Duration.ofSeconds(60L, 500_000_000),
+ READER.readValue(q("60.5")));
+ }
+
+ @Test
+ public void testIsoStringStillWorksWhenEnabled() throws Exception
+ {
+ assertEquals(Duration.ofSeconds(25L), READER.readValue(q("PT25S")));
+ }
+
+ @Test
+ public void testJsonFormatSecondsAppliesToStringifiedInteger() throws Exception
+ {
+ SecondsWrapper w = MAPPER.readerFor(SecondsWrapper.class)
+ .readValue("{\"value\":\"3600\"}");
+ assertEquals(Duration.ofSeconds(3600L), w.value);
+ }
+
+ @Test
+ public void testJsonFormatSecondsDoesNotApplyWhenDisabled() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> newMapper().readerFor(SecondsWrapper.class)
+ .readValue("{\"value\":\"3600\"}"));
+ }
+
+ @Test
+ public void testNonNumericStringStillFailsWhenEnabled() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> READER.readValue(q("not-a-duration")));
+ }
+}
diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index c8693d31..13b1d5ce 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -243,3 +243,12 @@ Seonwoo Jung (@seonwooj0810)
* Contributed fix for #76: Missing milliseconds, when serializing Java 8 date-time,
if they are zeros
(2.23.0)
+
+Jakub Bocheński (@jakub-bochenski)
+ * Reported #232: Deserialize Duration from int-like String (e.g. `"3600"`)
+ (2.23.0)
+
+Fardan An (@arimu1)
+ * Contributed #232: Add `JavaTimeFeature.ALLOW_STRINGIFIED_DURATION_VALUES`
+ to deserialize `Duration` from stringified numbers
+ (2.23.0)
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 6bcaa93e..dd565302 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)
+#232: Add `JavaTimeFeature.ALLOW_STRINGIFIED_DURATION_VALUES` to deserialize
+ `Duration` from stringified numbers (e.g. `"3600"`)
+ (reported by @jakub-bochenski)
+ (contributed by @arimu1)
2.22.2 (16-Aug-2026)
2.22.1 (07-Jul-2026)