Skip to content

Commit d8d0f5c

Browse files
authored
274: add MonthDeserializer and JavaTimeFeature option (#292)
1 parent f7780a4 commit d8d0f5c

File tree

10 files changed

+423
-3
lines changed

10 files changed

+423
-3
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ public enum JavaTimeFeature implements JacksonFeature
2929
* stringified numbers are always accepted as timestamps regardless of
3030
* this feature.
3131
*/
32-
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false)
32+
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false),
33+
34+
/**
35+
* Feature that determines whether {@link java.time.Month} is serialized
36+
* and deserialized as using a zero-based index (FALSE) or a one-based index (TRUE).
37+
* For example, "1" would be serialized/deserialized as Month.JANUARY if TRUE and Month.FEBRUARY if FALSE.
38+
*<p>
39+
* Default setting is false, meaning that Month is serialized/deserialized as a zero-based index.
40+
*/
41+
ONE_BASED_MONTHS(false)
3342
;
3443

3544
/**

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ public final class JavaTimeModule
101101
public JavaTimeModule()
102102
{
103103
super(PackageVersion.VERSION);
104-
105104
_features = JacksonFeatureSet.fromDefaults(JavaTimeFeature.values());
106105
}
107106

@@ -142,6 +141,11 @@ public void setupModule(SetupContext context) {
142141
desers.addDeserializer(ZoneOffset.class, JSR310StringParsableDeserializer.ZONE_OFFSET);
143142

144143
context.addDeserializers(desers);
144+
145+
final boolean oneBasedMonthEnabled = _features.isEnabled(JavaTimeFeature.ONE_BASED_MONTHS);
146+
147+
context.addBeanDeserializerModifier(new JavaTimeDeserializerModifier(oneBasedMonthEnabled));
148+
context.addBeanSerializerModifier(new JavaTimeSerializerModifier(oneBasedMonthEnabled));
145149
// 20-Nov-2023, tatu: [modules-java8#288]: someone may have directly
146150
// added entries, need to add for backwards compatibility
147151
if (_deserializers != null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.fasterxml.jackson.datatype.jsr310.deser;
2+
3+
import java.time.Month;
4+
5+
import com.fasterxml.jackson.databind.BeanDescription;
6+
import com.fasterxml.jackson.databind.DeserializationConfig;
7+
import com.fasterxml.jackson.databind.JavaType;
8+
import com.fasterxml.jackson.databind.JsonDeserializer;
9+
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
10+
11+
/**
12+
* @since 2.17
13+
*/
14+
public class JavaTimeDeserializerModifier extends BeanDeserializerModifier {
15+
private static final long serialVersionUID = 1L;
16+
17+
private final boolean _oneBaseMonths;
18+
19+
public JavaTimeDeserializerModifier(boolean oneBaseMonths) {
20+
_oneBaseMonths = oneBaseMonths;
21+
}
22+
23+
@Override
24+
public JsonDeserializer<?> modifyEnumDeserializer(DeserializationConfig config, JavaType type, BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer) {
25+
if (_oneBaseMonths && type.hasRawClass(Month.class)) {
26+
return new OneBasedMonthDeserializer(defaultDeserializer);
27+
}
28+
return defaultDeserializer;
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.fasterxml.jackson.datatype.jsr310.deser;
2+
3+
import java.io.IOException;
4+
import java.time.Month;
5+
import java.util.regex.Pattern;
6+
7+
import com.fasterxml.jackson.core.JsonParser;
8+
import com.fasterxml.jackson.core.JsonToken;
9+
import com.fasterxml.jackson.databind.DeserializationContext;
10+
import com.fasterxml.jackson.databind.JsonDeserializer;
11+
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
12+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
13+
14+
/**
15+
* @since 2.17
16+
*/
17+
public class OneBasedMonthDeserializer extends DelegatingDeserializer {
18+
private static final long serialVersionUID = 1L;
19+
20+
private static final Pattern HAS_ONE_OR_TWO_DIGITS = Pattern.compile("^\\d{1,2}$");
21+
22+
public OneBasedMonthDeserializer(JsonDeserializer<?> defaultDeserializer) {
23+
super(defaultDeserializer);
24+
}
25+
26+
@Override
27+
public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException {
28+
JsonToken token = parser.currentToken();
29+
Month zeroBaseMonth = (Month) getDelegatee().deserialize(parser, context);
30+
if (!_isNumericValue(parser.getText(), token)) {
31+
return zeroBaseMonth;
32+
}
33+
if (zeroBaseMonth == Month.JANUARY) {
34+
throw new InvalidFormatException(parser, "Month.JANUARY value not allowed for 1-based Month.", zeroBaseMonth, Month.class);
35+
}
36+
return zeroBaseMonth.minus(1);
37+
}
38+
39+
private boolean _isNumericValue(String text, JsonToken token) {
40+
return token == JsonToken.VALUE_NUMBER_INT || _isNumberAsString(text, token);
41+
}
42+
43+
private boolean _isNumberAsString(String text, JsonToken token) {
44+
return token == JsonToken.VALUE_STRING && HAS_ONE_OR_TWO_DIGITS.matcher(text).matches();
45+
}
46+
47+
@Override
48+
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
49+
return new OneBasedMonthDeserializer(newDelegatee);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.fasterxml.jackson.datatype.jsr310.ser;
2+
3+
import java.time.Month;
4+
5+
import com.fasterxml.jackson.databind.BeanDescription;
6+
import com.fasterxml.jackson.databind.JavaType;
7+
import com.fasterxml.jackson.databind.JsonSerializer;
8+
import com.fasterxml.jackson.databind.SerializationConfig;
9+
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
10+
11+
/**
12+
* @since 2.17
13+
*/
14+
public class JavaTimeSerializerModifier extends BeanSerializerModifier {
15+
private static final long serialVersionUID = 1L;
16+
17+
private final boolean _oneBaseMonths;
18+
19+
public JavaTimeSerializerModifier(boolean oneBaseMonths) {
20+
_oneBaseMonths = oneBaseMonths;
21+
}
22+
23+
@Override
24+
public JsonSerializer<?> modifyEnumSerializer(SerializationConfig config, JavaType valueType, BeanDescription beanDesc, JsonSerializer<?> serializer) {
25+
if (_oneBaseMonths && valueType.hasRawClass(Month.class)) {
26+
return new OneBasedMonthSerializer(serializer);
27+
}
28+
return serializer;
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.fasterxml.jackson.datatype.jsr310.ser;
2+
3+
import java.io.IOException;
4+
import java.time.Month;
5+
6+
import com.fasterxml.jackson.core.JsonGenerator;
7+
import com.fasterxml.jackson.databind.JsonSerializer;
8+
import com.fasterxml.jackson.databind.SerializationFeature;
9+
import com.fasterxml.jackson.databind.SerializerProvider;
10+
11+
/**
12+
* @since 2.17
13+
*/
14+
public class OneBasedMonthSerializer extends JsonSerializer<Month> {
15+
private final JsonSerializer<Object> _defaultSerializer;
16+
17+
@SuppressWarnings("unchecked")
18+
public OneBasedMonthSerializer(JsonSerializer<?> defaultSerializer)
19+
{
20+
_defaultSerializer = (JsonSerializer<Object>) defaultSerializer;
21+
}
22+
23+
@Override
24+
public void serialize(Month value, JsonGenerator gen, SerializerProvider ctxt)
25+
throws IOException
26+
{
27+
// 15-Jan-2024, tatu: [modules-java8#274] This is not really sufficient
28+
// (see `jackson-databind` `EnumSerializer` for full logic), but has to
29+
// do for now. May need to add `@JsonFormat.shape` handling in future.
30+
if (ctxt.isEnabled(SerializationFeature.WRITE_ENUMS_USING_INDEX)) {
31+
gen.writeNumber(value.ordinal() + 1);
32+
return;
33+
}
34+
_defaultSerializer.serialize(value, gen, ctxt);
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.fasterxml.jackson.datatype.jsr310.deser;
2+
3+
import java.time.Month;
4+
import java.time.temporal.TemporalAccessor;
5+
6+
import org.junit.Test;
7+
import org.junit.function.ThrowingRunnable;
8+
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.ObjectReader;
11+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
12+
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
13+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
14+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
15+
import com.fasterxml.jackson.databind.json.JsonMapper;
16+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;
17+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
18+
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
19+
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
20+
21+
import static org.junit.Assert.*;
22+
23+
public class OneBasedMonthDeserTest extends ModuleTestBase
24+
{
25+
static class Wrapper {
26+
public Month value;
27+
28+
public Wrapper(Month v) { value = v; }
29+
public Wrapper() { }
30+
}
31+
32+
@Test
33+
public void testDeserializationAsString01_oneBased() throws Exception
34+
{
35+
assertEquals(Month.JANUARY, readerForOneBased().readValue("\"01\""));
36+
}
37+
38+
@Test
39+
public void testDeserializationAsString01_zeroBased() throws Exception
40+
{
41+
assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("\"01\""));
42+
}
43+
44+
45+
@Test
46+
public void testDeserializationAsString02_oneBased() throws Exception
47+
{
48+
assertEquals(Month.JANUARY, readerForOneBased().readValue("\"JANUARY\""));
49+
}
50+
51+
@Test
52+
public void testDeserializationAsString02_zeroBased() throws Exception
53+
{
54+
assertEquals(Month.JANUARY, readerForZeroBased().readValue("\"JANUARY\""));
55+
}
56+
57+
@Test
58+
public void testBadDeserializationAsString01_oneBased() {
59+
assertError(
60+
() -> readerForOneBased().readValue("\"notamonth\""),
61+
InvalidFormatException.class,
62+
"Cannot deserialize value of type `java.time.Month` from String \"notamonth\": not one of the values accepted for Enum class: [OCTOBER, SEPTEMBER, JUNE, MARCH, MAY, APRIL, JULY, JANUARY, FEBRUARY, DECEMBER, AUGUST, NOVEMBER]"
63+
);
64+
}
65+
66+
static void assertError(ThrowingRunnable codeToRun, Class<? extends Throwable> expectedException, String expectedMessage) {
67+
try {
68+
codeToRun.run();
69+
fail(String.format("Expecting %s, but nothing was thrown!", expectedException.getName()));
70+
} catch (Throwable actualException) {
71+
if (!expectedException.isInstance(actualException)) {
72+
fail(String.format("Expecting exception of type %s, but %s was thrown instead", expectedException.getName(), actualException.getClass().getName()));
73+
}
74+
if (actualException.getMessage() == null || !actualException.getMessage().contains(expectedMessage)) {
75+
fail(String.format("Expecting exception with message containing:'%s', but the actual error message was:'%s'", expectedMessage, actualException.getMessage()));
76+
}
77+
}
78+
}
79+
80+
81+
@Test
82+
public void testDeserialization01_zeroBased() throws Exception
83+
{
84+
assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("1"));
85+
}
86+
87+
@Test
88+
public void testDeserialization01_oneBased() throws Exception
89+
{
90+
assertEquals(Month.JANUARY, readerForOneBased().readValue("1"));
91+
}
92+
93+
@Test
94+
public void testDeserialization02_zeroBased() throws Exception
95+
{
96+
assertEquals(Month.SEPTEMBER, readerForZeroBased().readValue("\"08\""));
97+
}
98+
99+
@Test
100+
public void testDeserialization02_oneBased() throws Exception
101+
{
102+
assertEquals(Month.AUGUST, readerForOneBased().readValue("\"08\""));
103+
}
104+
105+
@Test
106+
public void testDeserializationWithTypeInfo01_oneBased() throws Exception
107+
{
108+
ObjectMapper MAPPER = new ObjectMapper()
109+
.registerModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS));
110+
MAPPER.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class);
111+
112+
TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",11]", TemporalAccessor.class);
113+
assertEquals(Month.NOVEMBER, value);
114+
}
115+
116+
@Test
117+
public void testDeserializationWithTypeInfo01_zeroBased() throws Exception
118+
{
119+
ObjectMapper MAPPER = new ObjectMapper();
120+
MAPPER.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class);
121+
122+
TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
123+
assertEquals(Month.DECEMBER, value);
124+
}
125+
126+
@Test
127+
public void testFormatAnnotation_zeroBased() throws Exception
128+
{
129+
Wrapper output = readerForZeroBased().readValue("{\"value\":\"11\"}", Wrapper.class);
130+
assertEquals(new Wrapper(Month.DECEMBER).value, output.value);
131+
}
132+
133+
@Test
134+
public void testFormatAnnotation_oneBased() throws Exception
135+
{
136+
Wrapper output = readerForOneBased().readValue("{\"value\":\"11\"}", Wrapper.class);
137+
assertEquals(new Wrapper(Month.NOVEMBER).value, output.value);
138+
}
139+
140+
/*
141+
/**********************************************************
142+
/* Tests for empty string handling
143+
/**********************************************************
144+
*/
145+
146+
@Test
147+
public void testDeserializeFromEmptyString() throws Exception
148+
{
149+
final ObjectMapper mapper = newMapper();
150+
151+
// Nulls are handled in general way, not by deserializer so they are ok
152+
Month m = mapper.readerFor(Month.class).readValue(" null ");
153+
assertNull(m);
154+
155+
// But coercion from empty String not enabled for Enums by default:
156+
try {
157+
mapper.readerFor(Month.class).readValue("\"\"");
158+
fail("Should not pass");
159+
} catch (MismatchedInputException e) {
160+
verifyException(e, "Cannot coerce empty String");
161+
}
162+
// But can allow coercion of empty String to, say, null
163+
ObjectMapper emptyStringMapper = mapperBuilder()
164+
.withCoercionConfig(Month.class,
165+
h -> h.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull))
166+
.build();
167+
m = emptyStringMapper.readerFor(Month.class).readValue("\"\"");
168+
assertNull(m);
169+
}
170+
171+
private ObjectReader readerForZeroBased() {
172+
return JsonMapper.builder()
173+
.addModule(new JavaTimeModule()
174+
.disable(JavaTimeFeature.ONE_BASED_MONTHS))
175+
.build()
176+
.readerFor(Month.class);
177+
}
178+
179+
private ObjectReader readerForOneBased() {
180+
return JsonMapper.builder()
181+
.addModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS))
182+
.build()
183+
.readerFor(Month.class);
184+
}
185+
}

0 commit comments

Comments
 (0)