Skip to content

Commit 53f5dda

Browse files
authored
Fix #3013: Implement int-to-string coercion config (#3608)
1 parent 03f2605 commit 53f5dda

File tree

5 files changed

+159
-37
lines changed

5 files changed

+159
-37
lines changed

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

+36-4
Original file line numberDiff line numberDiff line change
@@ -1371,7 +1371,9 @@ protected java.util.Date _parseDate(String value, DeserializationContext ctxt)
13711371
*
13721372
* @since 2.1
13731373
*/
1374-
protected final String _parseString(JsonParser p, DeserializationContext ctxt) throws IOException
1374+
protected final String _parseString(JsonParser p, DeserializationContext ctxt,
1375+
NullValueProvider nullProvider)
1376+
throws IOException
13751377
{
13761378
if (p.hasToken(JsonToken.VALUE_STRING)) {
13771379
return p.getText();
@@ -1393,9 +1395,23 @@ protected final String _parseString(JsonParser p, DeserializationContext ctxt) t
13931395
return ctxt.extractScalarFromObject(p, this, _valueClass);
13941396
}
13951397

1396-
String value = p.getValueAsString();
1397-
if (value != null) {
1398-
return value;
1398+
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
1399+
final CoercionAction act = _checkIntToStringCoercion(p, ctxt, _valueClass);
1400+
if (act == CoercionAction.AsNull) {
1401+
return (String) nullProvider.getNullValue(ctxt);
1402+
}
1403+
if (act == CoercionAction.AsEmpty) {
1404+
return "";
1405+
}
1406+
}
1407+
// allow coercions for other scalar types
1408+
// 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
1409+
// "real" scalar
1410+
if (p.currentToken().isScalarValue()) {
1411+
String text = p.getValueAsString();
1412+
if (text != null) {
1413+
return text;
1414+
}
13991415
}
14001416
return (String) ctxt.handleUnexpectedToken(String.class, p);
14011417
}
@@ -1503,6 +1519,22 @@ protected CoercionAction _checkFloatToIntCoercion(JsonParser p, DeserializationC
15031519
return act;
15041520
}
15051521

1522+
/**
1523+
* @since 2.14
1524+
*/
1525+
protected CoercionAction _checkIntToStringCoercion(JsonParser p, DeserializationContext ctxt,
1526+
Class<?> rawTargetType)
1527+
throws IOException
1528+
{
1529+
final CoercionAction act = ctxt.findCoercionAction(LogicalType.Textual,
1530+
rawTargetType, CoercionInputShape.Integer);
1531+
if (act == CoercionAction.Fail) {
1532+
return _checkCoercionFail(ctxt, act, rawTargetType, p.getNumberValue(),
1533+
"Integer value (" + p.getText() + ")");
1534+
}
1535+
return act;
1536+
}
1537+
15061538
/**
15071539
* @since 2.14
15081540
*/

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public String[] deserialize(JsonParser p, DeserializationContext ctxt) throws IO
165165
}
166166
value = (String) _nullProvider.getNullValue(ctxt);
167167
} else {
168-
value = _parseString(p, ctxt);
168+
value = _parseString(p, ctxt, _nullProvider);
169169
}
170170
}
171171
if (ix >= chunk.length) {
@@ -286,7 +286,7 @@ public String[] deserialize(JsonParser p, DeserializationContext ctxt,
286286
}
287287
value = (String) _nullProvider.getNullValue(ctxt);
288288
} else {
289-
value = _parseString(p, ctxt);
289+
value = _parseString(p, ctxt, _nullProvider);
290290
}
291291
}
292292
if (ix >= chunk.length) {
@@ -335,7 +335,7 @@ private final String[] handleNonArray(JsonParser p, DeserializationContext ctxt)
335335
// if coercion failed, we can still add it to an array
336336
}
337337

338-
value = _parseString(p, ctxt);
338+
value = _parseString(p, ctxt, _nullProvider);
339339
}
340340
return new String[] { value };
341341
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public Collection<String> deserialize(JsonParser p, DeserializationContext ctxt,
215215
}
216216
value = (String) _nullProvider.getNullValue(ctxt);
217217
} else {
218-
value = _parseString(p, ctxt);
218+
value = _parseString(p, ctxt, _nullProvider);
219219
}
220220
result.add(value);
221221
}
@@ -322,7 +322,7 @@ private final Collection<String> handleNonArray(JsonParser p, DeserializationCon
322322
}
323323

324324
try {
325-
value = (valueDes == null) ? _parseString(p, ctxt) : valueDes.deserialize(p, ctxt);
325+
value = (valueDes == null) ? _parseString(p, ctxt, _nullProvider) : valueDes.deserialize(p, ctxt);
326326
} catch (Exception e) {
327327
throw JsonMappingException.wrapWithPath(e, result, result.size());
328328
}

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

+3-28
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.core.*;
66
import com.fasterxml.jackson.databind.*;
77
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
8+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
89
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
910
import com.fasterxml.jackson.databind.type.LogicalType;
1011

@@ -40,37 +41,11 @@ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
4041
if (p.hasToken(JsonToken.VALUE_STRING)) {
4142
return p.getText();
4243
}
43-
JsonToken t = p.currentToken();
4444
// [databind#381]
45-
if (t == JsonToken.START_ARRAY) {
45+
if (p.hasToken(JsonToken.START_ARRAY)) {
4646
return _deserializeFromArray(p, ctxt);
4747
}
48-
// need to gracefully handle byte[] data, as base64
49-
if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
50-
Object ob = p.getEmbeddedObject();
51-
if (ob == null) {
52-
return null;
53-
}
54-
if (ob instanceof byte[]) {
55-
return ctxt.getBase64Variant().encode((byte[]) ob, false);
56-
}
57-
// otherwise, try conversion using toString()...
58-
return ob.toString();
59-
}
60-
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
61-
if (t == JsonToken.START_OBJECT) {
62-
return ctxt.extractScalarFromObject(p, this, _valueClass);
63-
}
64-
// allow coercions for other scalar types
65-
// 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
66-
// "real" scalar
67-
if (t.isScalarValue()) {
68-
String text = p.getValueAsString();
69-
if (text != null) {
70-
return text;
71-
}
72-
}
73-
return (String) ctxt.handleUnexpectedToken(_valueClass, p);
48+
return _parseString(p, ctxt, this);
7449
}
7550

7651
// Since we can never have type info ("natural type"; String, Boolean, Integer, Double):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
12+
public class CoerceIntToStringTest extends BaseMapTest
13+
{
14+
private final ObjectMapper DEFAULT_MAPPER = newJsonMapper();
15+
16+
private final ObjectMapper MAPPER_TO_FAIL = jsonMapperBuilder()
17+
.withCoercionConfig(LogicalType.Textual, cfg ->
18+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail))
19+
.build();
20+
21+
private final ObjectMapper MAPPER_TRY_CONVERT = jsonMapperBuilder()
22+
.withCoercionConfig(LogicalType.Textual, cfg ->
23+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.TryConvert))
24+
.build();
25+
26+
private final ObjectMapper MAPPER_TO_NULL = jsonMapperBuilder()
27+
.withCoercionConfig(LogicalType.Textual, cfg ->
28+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsNull))
29+
.build();
30+
31+
private final ObjectMapper MAPPER_TO_EMPTY = jsonMapperBuilder()
32+
.withCoercionConfig(LogicalType.Textual, cfg ->
33+
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsEmpty))
34+
.build();
35+
36+
public void testDefaultIntToStringCoercion() throws JsonProcessingException
37+
{
38+
assertSuccessfulIntToStringCoercionWith(DEFAULT_MAPPER);
39+
}
40+
41+
public void testCoerceConfigToConvert() throws JsonProcessingException
42+
{
43+
assertSuccessfulIntToStringCoercionWith(MAPPER_TRY_CONVERT);
44+
}
45+
46+
public void testCoerceConfigToNull() throws JsonProcessingException
47+
{
48+
assertNull(MAPPER_TO_NULL.readValue("1", String.class));
49+
StringWrapper w = MAPPER_TO_NULL.readValue("{\"str\": -5}", StringWrapper.class);
50+
assertNull(w.str);
51+
String[] arr = MAPPER_TO_NULL.readValue("[ 2 ]", String[].class);
52+
assertEquals(1, arr.length);
53+
assertNull(arr[0]);
54+
}
55+
56+
public void testCoerceConfigToEmpty() throws JsonProcessingException
57+
{
58+
assertEquals("", MAPPER_TO_EMPTY.readValue("3", String.class));
59+
StringWrapper w = MAPPER_TO_EMPTY.readValue("{\"str\": -5}", StringWrapper.class);
60+
assertEquals("", w.str);
61+
String[] arr = MAPPER_TO_EMPTY.readValue("[ 2 ]", String[].class);
62+
assertEquals(1, arr.length);
63+
assertEquals("", arr[0]);
64+
}
65+
66+
public void testCoerceConfigToFail() throws JsonProcessingException
67+
{
68+
_verifyCoerceFail(MAPPER_TO_FAIL, String.class, "3");
69+
_verifyCoerceFail(MAPPER_TO_FAIL, StringWrapper.class, "{\"str\": -5}", "string");
70+
_verifyCoerceFail(MAPPER_TO_FAIL, String[].class, "[ 2 ]", "element of `java.lang.String[]`");
71+
}
72+
73+
/*
74+
/********************************************************
75+
/* Helper methods
76+
/********************************************************
77+
*/
78+
79+
private void assertSuccessfulIntToStringCoercionWith(ObjectMapper objectMapper)
80+
throws JsonProcessingException
81+
{
82+
assertEquals("3", objectMapper.readValue("3", String.class));
83+
assertEquals("-2", objectMapper.readValue("-2", String.class));
84+
{
85+
StringWrapper w = objectMapper.readValue("{\"str\": -5}", StringWrapper.class);
86+
assertEquals("-5", w.str);
87+
String[] arr = objectMapper.readValue("[ 2 ]", String[].class);
88+
assertEquals("2", arr[0]);
89+
}
90+
}
91+
92+
private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType,
93+
String doc) throws JsonProcessingException
94+
{
95+
_verifyCoerceFail(m.reader(), targetType, doc, targetType.getName());
96+
}
97+
98+
private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType,
99+
String doc, String targetTypeDesc) throws JsonProcessingException
100+
{
101+
_verifyCoerceFail(m.reader(), targetType, doc, targetTypeDesc);
102+
}
103+
104+
private void _verifyCoerceFail(ObjectReader r, Class<?> targetType,
105+
String doc, String targetTypeDesc) throws JsonProcessingException
106+
{
107+
try {
108+
r.forType(targetType).readValue(doc);
109+
fail("Should not accept Integer for "+targetType.getName()+" when configured to");
110+
} catch (MismatchedInputException e) {
111+
verifyException(e, "Cannot coerce Integer");
112+
verifyException(e, targetTypeDesc);
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)