diff --git a/src/main/java/tools/jackson/databind/cfg/CoercionConfigs.java b/src/main/java/tools/jackson/databind/cfg/CoercionConfigs.java index 74873cf285..6e1152f5d7 100644 --- a/src/main/java/tools/jackson/databind/cfg/CoercionConfigs.java +++ b/src/main/java/tools/jackson/databind/cfg/CoercionConfigs.java @@ -158,8 +158,6 @@ public MutableCoercionConfig findOrCreateCoercion(Class type) { * @param inputShape Input shape to coerce from * * @return CoercionAction configured for specified coercion - * - * @since 2.12 */ public CoercionAction findCoercion(DeserializationConfig config, LogicalType targetType, @@ -221,7 +219,7 @@ public CoercionAction findCoercion(DeserializationConfig config, final boolean baseScalar = _isScalarType(targetType); if (baseScalar - // Default for setting in 2.x is true + // Default for setting in 2.x and 3.x is true && !config.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS) // 12-Oct-2022, carterkozak: As per [databind#3624]: Coercion from integer-shaped // data into a floating point type is not banned by the diff --git a/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java b/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java index 618e3e680d..0bd99d9c71 100644 --- a/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java +++ b/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java @@ -102,6 +102,7 @@ public void setupModule(JacksonModule.SetupContext context) { .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE) .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE) .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE) + .addDeserializer(Month.class, MonthDeserializer.INSTANCE) .addDeserializer(MonthDay.class, MonthDayDeserializer.INSTANCE) .addDeserializer(OffsetTime.class, OffsetTimeDeserializer.INSTANCE) .addDeserializer(Period.class, JSR310StringParsableDeserializer.PERIOD) @@ -118,6 +119,7 @@ public void setupModule(JacksonModule.SetupContext context) { .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE) .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE) +// .addSerializer(Month.class, MonthSerializer.INSTANCE) .addSerializer(MonthDay.class, MonthDaySerializer.INSTANCE) .addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE) .addSerializer(OffsetTime.class, OffsetTimeSerializer.INSTANCE) @@ -159,7 +161,6 @@ public void setupModule(JacksonModule.SetupContext context) { // [modules-java8#274]: 1-based Month (de)serializer need to be applied via modifiers: // [databind#5078]: Should rewrite not to require this - context.addDeserializerModifier(new JavaTimeDeserializerModifier()); context.addSerializerModifier(new JavaTimeSerializerModifier()); context.addValueInstantiators(new ValueInstantiators.Base() { diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java deleted file mode 100644 index 831900a7f6..0000000000 --- a/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java +++ /dev/null @@ -1,25 +0,0 @@ -package tools.jackson.databind.ext.javatime.deser; - -import java.time.Month; - -import tools.jackson.databind.*; -import tools.jackson.databind.deser.ValueDeserializerModifier; - -/* 08-Apr-2025, tatu: we really should rewrite things to have "native" - * {@code MonthDeserializer} and not rely on {@code EnumDeserializer}. - */ -public class JavaTimeDeserializerModifier extends ValueDeserializerModifier -{ - private static final long serialVersionUID = 1L; - - public JavaTimeDeserializerModifier() { } - - @Override - public ValueDeserializer modifyEnumDeserializer(DeserializationConfig config, JavaType type, - BeanDescription.Supplier beanDescRef, ValueDeserializer defaultDeserializer) { - if (type.hasRawClass(Month.class)) { - return new OneBasedMonthDeserializer(defaultDeserializer); - } - return defaultDeserializer; - } -} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDeserializer.java new file mode 100644 index 0000000000..748ae883d8 --- /dev/null +++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDeserializer.java @@ -0,0 +1,171 @@ +package tools.jackson.databind.ext.javatime.deser; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +import tools.jackson.core.*; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.exc.InvalidFormatException; + +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * Deserializer for Java 8 temporal {@link Month}s. + */ +public class MonthDeserializer extends JSR310DateTimeDeserializerBase +{ + public static final MonthDeserializer INSTANCE = new MonthDeserializer(); + + private final Set possibleMonthStringValues = Arrays.stream(Month.values()).map(Month::name).collect(Collectors.toSet()); + + /** + * NOTE: only {@code public} so that use via annotations (see [modules-java8#202]) + * is possible + */ + public MonthDeserializer() { + this(null); + } + + public MonthDeserializer(DateTimeFormatter formatter) { + super(Month.class, formatter); + } + + protected MonthDeserializer(MonthDeserializer base, Boolean leniency) { + super(base, leniency); + } + + protected MonthDeserializer(MonthDeserializer base, + Boolean leniency, + DateTimeFormatter formatter, + JsonFormat.Shape shape) { + super(base, leniency, formatter, shape); + } + + @Override + protected MonthDeserializer withLeniency(Boolean leniency) { + return new MonthDeserializer(this, leniency); + } + + @Override + protected MonthDeserializer withDateFormat(DateTimeFormatter dtf) { + return new MonthDeserializer(this, _isLenient, dtf, _shape); + } + + @Override + public Month deserialize(JsonParser parser, DeserializationContext context) + throws JacksonException + { + if (parser.hasToken(JsonToken.VALUE_STRING)) { + return _fromString(parser, context, parser.getString()); + } + // Support numeric scalar input + if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + final int raw = parser.getIntValue(); + if (context.isEnabled(DateTimeFeature.ONE_BASED_MONTHS)) { + return _decodeMonth(raw, context); + } + // default: 0‑based index (0 == JANUARY) + if (raw < 0 || raw >= 12) { + context.handleWeirdNumberValue(handledType(), + raw, "Month index (%s) outside 0-11 range", raw); + return null; // never gets here, but compiler doesn't know + } + return Month.values()[raw]; + } + // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (parser.isExpectedStartObjectToken()) { + return _fromString(parser, context, + context.extractScalarFromObject(parser, this, handledType())); + } + if (parser.isExpectedStartArrayToken()) { + JsonToken t = parser.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT) + && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + final Month parsed = deserialize(parser, context); + if (parser.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(parser, context); + } + return parsed; + } + if (t != JsonToken.VALUE_NUMBER_INT) { + _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, Integer.class.getName()); + } + int month = parser.getIntValue(); + if (parser.nextToken() != JsonToken.END_ARRAY) { + throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, + "Expected array to end"); + } + return Month.of(month); + } + if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { + return (Month) parser.getEmbeddedObject(); + } + return _handleUnexpectedToken(context, parser, + JsonToken.VALUE_STRING, JsonToken.START_ARRAY); + } + + protected Month _fromString(JsonParser p, DeserializationContext ctxt, + String string0) + throws JacksonException + { + String string = string0.trim(); + if (string.length() == 0) { + // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish + // b/w empty and blank); for now don't which will allow blanks to be + // handled like "regular" empty (same as pre-2.12) + return _fromEmptyString(p, ctxt, string); + } + try { + if (_formatter == null) { + // First: try purely numeric input + try { + int oneBasedMonthNumber = Integer.parseInt(string); + if (ctxt.isEnabled(DateTimeFeature.ONE_BASED_MONTHS)) { + return _decodeMonth(oneBasedMonthNumber, ctxt); + } + if (oneBasedMonthNumber < 0 || oneBasedMonthNumber >= 12) { // invalid for 0‑based + throw new InvalidFormatException(p, "Month number " + oneBasedMonthNumber + " not allowed for 1-based Month.", oneBasedMonthNumber, Integer.class); + } + return Month.values()[oneBasedMonthNumber]; // 0‑based mapping + } catch (NumberFormatException nfe) { + // fall through – treat as textual month name + } + // Second: try textual input + // Handle English month names such as "JANUARY" from the actual Month Enum names + if (possibleMonthStringValues.contains(string)) { + return Month.valueOf(string); + } else { + throw new InvalidFormatException(p, String.format("Cannot deserialize value of type `java.time.Month` from String \"%s\": not one of the values accepted for Enum class: %s", string, Arrays.toString(Month.values())), string, Month.class); + } + } + return Month.from(_formatter.parse(string)); + } catch (DateTimeException e) { + return _handleDateTimeFormatException(ctxt, e, _formatter, string); + } catch (NumberFormatException e) { + throw ctxt.weirdStringException(string, handledType(), + "not a valid month value"); + } + } + + /** + * Validate and convert a 1‑based month number to {@link Month}. + */ + private Month _decodeMonth(int oneBasedMonthNumber, DeserializationContext ctxt) + throws JacksonException + { + if (Month.JANUARY.getValue() <= oneBasedMonthNumber && oneBasedMonthNumber <= Month.DECEMBER.getValue()) { + return Month.of(oneBasedMonthNumber); + } + // If out of range, throw an exception + ctxt.handleWeirdNumberValue(handledType(), + oneBasedMonthNumber, "Month number %s not allowed for 1-based Month.", oneBasedMonthNumber); + return null; // never gets here, but compiler doesn't know + } +} diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java deleted file mode 100644 index 566c3d3f7f..0000000000 --- a/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java +++ /dev/null @@ -1,76 +0,0 @@ -package tools.jackson.databind.ext.javatime.deser; - -import java.time.Month; - -import tools.jackson.core.JsonParser; -import tools.jackson.core.JsonToken; - -import tools.jackson.databind.*; -import tools.jackson.databind.cfg.DateTimeFeature; -import tools.jackson.databind.deser.std.DelegatingDeserializer; -import tools.jackson.databind.exc.InvalidFormatException; - -public class OneBasedMonthDeserializer extends DelegatingDeserializer { - public OneBasedMonthDeserializer(ValueDeserializer defaultDeserializer) { - super(defaultDeserializer); - } - - @Override - public Object deserialize(JsonParser parser, DeserializationContext context) { - final boolean oneBased = context.isEnabled(DateTimeFeature.ONE_BASED_MONTHS); - if (oneBased) { - JsonToken token = parser.currentToken(); - switch (token) { - case VALUE_NUMBER_INT: - return _decodeMonth(parser.getIntValue(), parser); - case VALUE_STRING: - String monthSpec = parser.getString(); - int oneBasedMonthNumber = _decodeNumber(monthSpec); - if (oneBasedMonthNumber >= 0) { - return _decodeMonth(oneBasedMonthNumber, parser); - } - default: - // Otherwise fall through to default handling - break; - } - // fall-through - } - return getDelegatee().deserialize(parser, context); - } - - /** - * @return Numeric value of input text that represents a 1-digit or 2-digit number. - * Negative value in other cases (empty string, not a number, 3 or more digits). - */ - private int _decodeNumber(String text) { - int numValue; - switch (text.length()) { - case 1: - char c = text.charAt(0); - boolean cValid = ('0' <= c && c <= '9'); - numValue = cValid ? (c - '0') : -1; - break; - case 2: - char c1 = text.charAt(0); - char c2 = text.charAt(1); - boolean c12valid = ('0' <= c1 && c1 <= '9' && '0' <= c2 && c2 <= '9'); - numValue = c12valid ? (10 * (c1 - '0') + (c2 - '0')) : -1; - break; - default: - numValue = -1; - } - return numValue; - } - - private Month _decodeMonth(int oneBasedMonthNumber, JsonParser parser) throws InvalidFormatException { - if (Month.JANUARY.getValue() <= oneBasedMonthNumber && oneBasedMonthNumber <= Month.DECEMBER.getValue()) { - return Month.of(oneBasedMonthNumber); - } - throw new InvalidFormatException(parser, "Month number " + oneBasedMonthNumber + " not allowed for 1-based Month.", oneBasedMonthNumber, Integer.class); - } - - @Override - protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { - return new OneBasedMonthDeserializer(newDelegatee); - } -} diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java index 684fcb4427..8a67bc5c99 100644 --- a/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java +++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectReader; import tools.jackson.databind.cfg.CoercionAction; @@ -93,8 +94,8 @@ static void assertError(Executable codeToRun, Class expecte } } - private final ObjectMapper MAPPER = newJsonMapper(); - + private final ObjectMapper MAPPER = newMapper(); + @Test public void testDeserialization01_zeroBased() throws Exception { @@ -170,16 +171,20 @@ public void testFormatAnnotation_oneBased() throws Exception @Test public void testDeserializeFromEmptyString() throws Exception { - final ObjectMapper mapper = newMapper(); - // Nulls are handled in general way, not by deserializer so they are ok - Month m = mapper.readerFor(Month.class).readValue(" null "); + Month m = MAPPER.readerFor(Month.class).readValue(" null "); assertNull(m); - // But coercion from empty String not enabled for Enums by default: + // Although coercion from empty String not enabled for Enums by default, + // it IS for Scalars (when `MapperFeature.ALLOW_COERCION_OF_SCALARS` enabled + // which it is by default). So need to disable it here: + // (we no longer consider `Month` as LogicalType.Enum but LogicalType.DateTime) try { - mapper.readerFor(Month.class).readValue("\"\""); - fail("Should not pass"); + ObjectMapper strictMapper = mapperBuilder() + .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS) + .build(); + Month result = strictMapper.readerFor(Month.class).readValue("\"\""); + fail("Should not pass, but got: " + result); } catch (MismatchedInputException e) { verifyException(e, "Cannot coerce empty String"); }