Skip to content

Fix: 5078 Add new MonthDeserializer #5122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<Month>
{
public static final MonthDeserializer INSTANCE = new MonthDeserializer();

private final Set<String> 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
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,8 +94,8 @@ static void assertError(Executable codeToRun, Class<? extends Throwable> expecte
}
}

private final ObjectMapper MAPPER = newJsonMapper();
private final ObjectMapper MAPPER = newMapper();

@Test
public void testDeserialization01_zeroBased() throws Exception
{
Expand Down Expand Up @@ -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");
}
Expand Down