Skip to content

Commit 1e90547

Browse files
committed
Fix #3503: Implement int-to-float coercion config
1 parent f6704e3 commit 1e90547

File tree

4 files changed

+238
-3
lines changed

4 files changed

+238
-3
lines changed

src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java

+27-3
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,16 @@ protected final Float _parseFloat(JsonParser p, DeserializationContext ctxt)
611611
break;
612612
case JsonTokenId.ID_NULL: // null fine for non-primitive
613613
return (Float) getNullValue(ctxt);
614+
case JsonTokenId.ID_NUMBER_INT:
615+
final CoercionAction act = _checkIntToFloatCoercion(p, ctxt, _valueClass);
616+
if (act == CoercionAction.AsNull) {
617+
return (Float) getNullValue(ctxt);
618+
}
619+
if (act == CoercionAction.AsEmpty) {
620+
return (Float) getEmptyValue(ctxt);
621+
}
622+
// fall through to coerce
614623
case JsonTokenId.ID_NUMBER_FLOAT:
615-
case JsonTokenId.ID_NUMBER_INT: // safe coercion
616624
return p.getFloatValue();
617625
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
618626
case JsonTokenId.ID_START_OBJECT:
@@ -700,8 +708,16 @@ protected final Double _parseDouble(JsonParser p, DeserializationContext ctxt) t
700708
break;
701709
case JsonTokenId.ID_NULL: // null fine for non-primitive
702710
return (Double) getNullValue(ctxt);
703-
case JsonTokenId.ID_NUMBER_FLOAT:
704-
case JsonTokenId.ID_NUMBER_INT: // safe coercion
711+
case JsonTokenId.ID_NUMBER_INT:
712+
final CoercionAction act = _checkIntToFloatCoercion(p, ctxt, _valueClass);
713+
if (act == CoercionAction.AsNull) {
714+
return (Double) getNullValue(ctxt);
715+
}
716+
if (act == CoercionAction.AsEmpty) {
717+
return (Double) getEmptyValue(ctxt);
718+
}
719+
// fall through to coerce
720+
case JsonTokenId.ID_NUMBER_FLOAT: // safe coercion
705721
return p.getDoubleValue();
706722
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
707723
case JsonTokenId.ID_START_OBJECT:
@@ -977,6 +993,14 @@ public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt)
977993
String text;
978994
switch (p.currentTokenId()) {
979995
case JsonTokenId.ID_NUMBER_INT:
996+
final CoercionAction act = _checkIntToFloatCoercion(p, ctxt, _valueClass);
997+
if (act == CoercionAction.AsNull) {
998+
return (BigDecimal) getNullValue(ctxt);
999+
}
1000+
if (act == CoercionAction.AsEmpty) {
1001+
return (BigDecimal) getEmptyValue(ctxt);
1002+
}
1003+
// fall through to coerce
9801004
case JsonTokenId.ID_NUMBER_FLOAT:
9811005
return p.getDecimalValue();
9821006
case JsonTokenId.ID_STRING:

src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java

+32
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,14 @@ protected final float _parseFloatPrimitive(JsonParser p, DeserializationContext
984984
text = p.getText();
985985
break;
986986
case JsonTokenId.ID_NUMBER_INT:
987+
final CoercionAction act = _checkIntToFloatCoercion(p, ctxt, Float.TYPE);
988+
if (act == CoercionAction.AsNull) {
989+
return 0.0f;
990+
}
991+
if (act == CoercionAction.AsEmpty) {
992+
return 0.0f;
993+
}
994+
// fall through to coerce
987995
case JsonTokenId.ID_NUMBER_FLOAT:
988996
return p.getFloatValue();
989997
case JsonTokenId.ID_NULL:
@@ -1105,6 +1113,14 @@ protected final double _parseDoublePrimitive(JsonParser p, DeserializationContex
11051113
text = p.getText();
11061114
break;
11071115
case JsonTokenId.ID_NUMBER_INT:
1116+
final CoercionAction act = _checkIntToFloatCoercion(p, ctxt, Double.TYPE);
1117+
if (act == CoercionAction.AsNull) {
1118+
return 0.0d;
1119+
}
1120+
if (act == CoercionAction.AsEmpty) {
1121+
return 0.0d;
1122+
}
1123+
// fall through to coerce
11081124
case JsonTokenId.ID_NUMBER_FLOAT:
11091125
return p.getDoubleValue();
11101126
case JsonTokenId.ID_NULL:
@@ -1474,6 +1490,22 @@ protected CoercionAction _checkFloatToIntCoercion(JsonParser p, DeserializationC
14741490
return act;
14751491
}
14761492

1493+
/**
1494+
* @since 2.14
1495+
*/
1496+
protected CoercionAction _checkIntToFloatCoercion(JsonParser p, DeserializationContext ctxt,
1497+
Class<?> rawTargetType)
1498+
throws IOException
1499+
{
1500+
final CoercionAction act = ctxt.findCoercionAction(LogicalType.Float,
1501+
rawTargetType, CoercionInputShape.Integer);
1502+
if (act == CoercionAction.Fail) {
1503+
return _checkCoercionFail(ctxt, act, rawTargetType, p.getNumberValue(),
1504+
"Integer value (" + p.getText() + ")");
1505+
}
1506+
return act;
1507+
}
1508+
14771509
/**
14781510
* @since 2.12
14791511
*/

src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java

+7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ public LongWrapper() { }
5757
public LongWrapper(long value) { l = value; }
5858
}
5959

60+
protected static class FloatWrapper {
61+
public float f;
62+
63+
public FloatWrapper() { }
64+
public FloatWrapper(float value) { f = value; }
65+
}
66+
6067
protected static class DoubleWrapper {
6168
public double d;
6269

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.fasterxml.jackson.databind.convert;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.BaseMapTest;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.ObjectReader;
7+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
8+
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
9+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
10+
import com.fasterxml.jackson.databind.type.LogicalType;
11+
import java.math.BigDecimal;
12+
13+
public class CoerceIntToFloatTest extends BaseMapTest
14+
{
15+
private final ObjectMapper DEFAULT_MAPPER = newJsonMapper();
16+
17+
private final ObjectMapper MAPPER_TO_FAIL = jsonMapperBuilder()
18+
.withCoercionConfig(LogicalType.Float, cfg ->
19+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail))
20+
.build();
21+
22+
private final ObjectMapper MAPPER_TRY_CONVERT = jsonMapperBuilder()
23+
.withCoercionConfig(LogicalType.Float, cfg ->
24+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.TryConvert))
25+
.build();
26+
27+
private final ObjectMapper MAPPER_TO_NULL = jsonMapperBuilder()
28+
.withCoercionConfig(LogicalType.Float, cfg ->
29+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsNull))
30+
.build();
31+
32+
private final ObjectMapper MAPPER_TO_EMPTY = jsonMapperBuilder()
33+
.withCoercionConfig(LogicalType.Float, cfg ->
34+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsEmpty))
35+
.build();
36+
37+
public void testDefaultIntToFloatCoercion() throws JsonProcessingException
38+
{
39+
assertSuccessfulIntToFloatConversionsWith(DEFAULT_MAPPER);
40+
}
41+
42+
public void testCoerceConfigToConvert() throws JsonProcessingException
43+
{
44+
assertSuccessfulIntToFloatConversionsWith(MAPPER_TRY_CONVERT);
45+
}
46+
47+
public void testCoerceConfigToNull() throws JsonProcessingException
48+
{
49+
assertNull(MAPPER_TO_NULL.readValue("1", Float.class));
50+
// `null` not possible for primitives, must use empty (aka default) value
51+
assertEquals(0.0f, MAPPER_TO_NULL.readValue("-2", Float.TYPE));
52+
{
53+
FloatWrapper w = MAPPER_TO_NULL.readValue("{\"f\": -5}", FloatWrapper.class);
54+
assertEquals(0.0f, w.f);
55+
float[] arr = MAPPER_TO_NULL.readValue("[ 2 ]", float[].class);
56+
assertEquals(1, arr.length);
57+
assertEquals(0.0f, arr[0]);
58+
}
59+
60+
assertNull(MAPPER_TO_NULL.readValue("-1", Double.class));
61+
assertEquals(0.0d, MAPPER_TO_NULL.readValue("4", Double.TYPE));
62+
{
63+
DoubleWrapper w = MAPPER_TO_NULL.readValue("{\"d\": 2}", DoubleWrapper.class);
64+
assertEquals(0.0d, w.d);
65+
double[] arr = MAPPER_TO_NULL.readValue("[ -7 ]", double[].class);
66+
assertEquals(1, arr.length);
67+
assertEquals(0.0d, arr[0]);
68+
}
69+
70+
assertNull(MAPPER_TO_NULL.readValue("420", BigDecimal.class));
71+
{
72+
BigDecimal[] arr = MAPPER_TO_NULL.readValue("[ 420 ]", BigDecimal[].class);
73+
assertEquals(1, arr.length);
74+
assertNull(arr[0]);
75+
}
76+
}
77+
78+
public void testCoerceConfigToEmpty() throws JsonProcessingException
79+
{
80+
assertEquals(0.0f, MAPPER_TO_EMPTY.readValue("3", Float.class));
81+
assertEquals(0.0f, MAPPER_TO_EMPTY.readValue("-2", Float.TYPE));
82+
{
83+
FloatWrapper w = MAPPER_TO_EMPTY.readValue("{\"f\": -5}", FloatWrapper.class);
84+
assertEquals(0.0f, w.f);
85+
float[] arr = MAPPER_TO_EMPTY.readValue("[ 2 ]", float[].class);
86+
assertEquals(1, arr.length);
87+
assertEquals(0.0f, arr[0]);
88+
}
89+
90+
assertEquals(0.0d, MAPPER_TO_EMPTY.readValue("-1", Double.class));
91+
assertEquals(0.0d, MAPPER_TO_EMPTY.readValue("-5", Double.TYPE));
92+
{
93+
DoubleWrapper w = MAPPER_TO_EMPTY.readValue("{\"d\": 2}", DoubleWrapper.class);
94+
assertEquals(0.0d, w.d);
95+
double[] arr = MAPPER_TO_EMPTY.readValue("[ -2 ]", double[].class);
96+
assertEquals(1, arr.length);
97+
assertEquals(0.0d, arr[0]);
98+
}
99+
100+
assertEquals(BigDecimal.valueOf(0), MAPPER_TO_EMPTY.readValue("3643", BigDecimal.class));
101+
}
102+
103+
public void testCoerceConfigToFail() throws JsonProcessingException
104+
{
105+
_verifyCoerceFail(MAPPER_TO_FAIL, Float.class, "3");
106+
_verifyCoerceFail(MAPPER_TO_FAIL, Float.TYPE, "-2");
107+
_verifyCoerceFail(MAPPER_TO_FAIL, FloatWrapper.class, "{\"f\": -5}", "float");
108+
_verifyCoerceFail(MAPPER_TO_FAIL, float[].class, "[ 2 ]", "element of `float[]`");
109+
110+
_verifyCoerceFail(MAPPER_TO_FAIL, Double.class, "-1");
111+
_verifyCoerceFail(MAPPER_TO_FAIL, Double.TYPE, "4");
112+
_verifyCoerceFail(MAPPER_TO_FAIL, DoubleWrapper.class, "{\"d\": 2}", "double");
113+
_verifyCoerceFail(MAPPER_TO_FAIL, double[].class, "[ -2 ]", "element of `double[]`");
114+
115+
_verifyCoerceFail(MAPPER_TO_FAIL, BigDecimal.class, "73455342");
116+
}
117+
118+
/*
119+
/********************************************************
120+
/* Helper methods
121+
/********************************************************
122+
*/
123+
124+
private void assertSuccessfulIntToFloatConversionsWith(ObjectMapper objectMapper)
125+
throws JsonProcessingException
126+
{
127+
assertEquals(3.0f, objectMapper.readValue("3", Float.class));
128+
assertEquals(-2.0f, objectMapper.readValue("-2", Float.TYPE));
129+
{
130+
FloatWrapper w = objectMapper.readValue("{\"f\": -5}", FloatWrapper.class);
131+
assertEquals(-5.0f, w.f);
132+
float[] arr = objectMapper.readValue("[ 2 ]", float[].class);
133+
assertEquals(2.0f, arr[0]);
134+
}
135+
136+
assertEquals(-1.0d, objectMapper.readValue("-1", Double.class));
137+
assertEquals(4.0d, objectMapper.readValue("4", Double.TYPE));
138+
{
139+
DoubleWrapper w = objectMapper.readValue("{\"d\": 2}", DoubleWrapper.class);
140+
assertEquals(2.0d, w.d);
141+
double[] arr = objectMapper.readValue("[ -2 ]", double[].class);
142+
assertEquals(-2.0d, arr[0]);
143+
}
144+
145+
BigDecimal biggie = objectMapper.readValue("423451233", BigDecimal.class);
146+
assertEquals(BigDecimal.valueOf(423451233.0d), biggie);
147+
}
148+
149+
private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType,
150+
String doc) throws JsonProcessingException
151+
{
152+
_verifyCoerceFail(m.reader(), targetType, doc, targetType.getName());
153+
}
154+
155+
private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType,
156+
String doc, String targetTypeDesc) throws JsonProcessingException
157+
{
158+
_verifyCoerceFail(m.reader(), targetType, doc, targetTypeDesc);
159+
}
160+
161+
private void _verifyCoerceFail(ObjectReader r, Class<?> targetType,
162+
String doc, String targetTypeDesc) throws JsonProcessingException
163+
{
164+
try {
165+
r.forType(targetType).readValue(doc);
166+
fail("Should not accept Integer for "+targetType.getName()+" when configured to");
167+
} catch (MismatchedInputException e) {
168+
verifyException(e, "Cannot coerce Integer");
169+
verifyException(e, targetTypeDesc);
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)