Skip to content

Commit

Permalink
Fix #3013: Implement int-to-string coercion config (#3608)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomasito665 authored Oct 2, 2022
1 parent 03f2605 commit 53f5dda
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,9 @@ protected java.util.Date _parseDate(String value, DeserializationContext ctxt)
*
* @since 2.1
*/
protected final String _parseString(JsonParser p, DeserializationContext ctxt) throws IOException
protected final String _parseString(JsonParser p, DeserializationContext ctxt,
NullValueProvider nullProvider)
throws IOException
{
if (p.hasToken(JsonToken.VALUE_STRING)) {
return p.getText();
Expand All @@ -1393,9 +1395,23 @@ protected final String _parseString(JsonParser p, DeserializationContext ctxt) t
return ctxt.extractScalarFromObject(p, this, _valueClass);
}

String value = p.getValueAsString();
if (value != null) {
return value;
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
final CoercionAction act = _checkIntToStringCoercion(p, ctxt, _valueClass);
if (act == CoercionAction.AsNull) {
return (String) nullProvider.getNullValue(ctxt);
}
if (act == CoercionAction.AsEmpty) {
return "";
}
}
// allow coercions for other scalar types
// 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
// "real" scalar
if (p.currentToken().isScalarValue()) {
String text = p.getValueAsString();
if (text != null) {
return text;
}
}
return (String) ctxt.handleUnexpectedToken(String.class, p);
}
Expand Down Expand Up @@ -1503,6 +1519,22 @@ protected CoercionAction _checkFloatToIntCoercion(JsonParser p, DeserializationC
return act;
}

/**
* @since 2.14
*/
protected CoercionAction _checkIntToStringCoercion(JsonParser p, DeserializationContext ctxt,
Class<?> rawTargetType)
throws IOException
{
final CoercionAction act = ctxt.findCoercionAction(LogicalType.Textual,
rawTargetType, CoercionInputShape.Integer);
if (act == CoercionAction.Fail) {
return _checkCoercionFail(ctxt, act, rawTargetType, p.getNumberValue(),
"Integer value (" + p.getText() + ")");
}
return act;
}

/**
* @since 2.14
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public String[] deserialize(JsonParser p, DeserializationContext ctxt) throws IO
}
value = (String) _nullProvider.getNullValue(ctxt);
} else {
value = _parseString(p, ctxt);
value = _parseString(p, ctxt, _nullProvider);
}
}
if (ix >= chunk.length) {
Expand Down Expand Up @@ -286,7 +286,7 @@ public String[] deserialize(JsonParser p, DeserializationContext ctxt,
}
value = (String) _nullProvider.getNullValue(ctxt);
} else {
value = _parseString(p, ctxt);
value = _parseString(p, ctxt, _nullProvider);
}
}
if (ix >= chunk.length) {
Expand Down Expand Up @@ -335,7 +335,7 @@ private final String[] handleNonArray(JsonParser p, DeserializationContext ctxt)
// if coercion failed, we can still add it to an array
}

value = _parseString(p, ctxt);
value = _parseString(p, ctxt, _nullProvider);
}
return new String[] { value };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public Collection<String> deserialize(JsonParser p, DeserializationContext ctxt,
}
value = (String) _nullProvider.getNullValue(ctxt);
} else {
value = _parseString(p, ctxt);
value = _parseString(p, ctxt, _nullProvider);
}
result.add(value);
}
Expand Down Expand Up @@ -322,7 +322,7 @@ private final Collection<String> handleNonArray(JsonParser p, DeserializationCon
}

try {
value = (valueDes == null) ? _parseString(p, ctxt) : valueDes.deserialize(p, ctxt);
value = (valueDes == null) ? _parseString(p, ctxt, _nullProvider) : valueDes.deserialize(p, ctxt);
} catch (Exception e) {
throw JsonMappingException.wrapWithPath(e, result, result.size());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.type.LogicalType;

Expand Down Expand Up @@ -40,37 +41,11 @@ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
if (p.hasToken(JsonToken.VALUE_STRING)) {
return p.getText();
}
JsonToken t = p.currentToken();
// [databind#381]
if (t == JsonToken.START_ARRAY) {
if (p.hasToken(JsonToken.START_ARRAY)) {
return _deserializeFromArray(p, ctxt);
}
// need to gracefully handle byte[] data, as base64
if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
Object ob = p.getEmbeddedObject();
if (ob == null) {
return null;
}
if (ob instanceof byte[]) {
return ctxt.getBase64Variant().encode((byte[]) ob, false);
}
// otherwise, try conversion using toString()...
return ob.toString();
}
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
if (t == JsonToken.START_OBJECT) {
return ctxt.extractScalarFromObject(p, this, _valueClass);
}
// allow coercions for other scalar types
// 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
// "real" scalar
if (t.isScalarValue()) {
String text = p.getValueAsString();
if (text != null) {
return text;
}
}
return (String) ctxt.handleUnexpectedToken(_valueClass, p);
return _parseString(p, ctxt, this);
}

// Since we can never have type info ("natural type"; String, Boolean, Integer, Double):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.fasterxml.jackson.databind.convert;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.type.LogicalType;

public class CoerceIntToStringTest extends BaseMapTest
{
private final ObjectMapper DEFAULT_MAPPER = newJsonMapper();

private final ObjectMapper MAPPER_TO_FAIL = jsonMapperBuilder()
.withCoercionConfig(LogicalType.Textual, cfg ->
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail))
.build();

private final ObjectMapper MAPPER_TRY_CONVERT = jsonMapperBuilder()
.withCoercionConfig(LogicalType.Textual, cfg ->
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.TryConvert))
.build();

private final ObjectMapper MAPPER_TO_NULL = jsonMapperBuilder()
.withCoercionConfig(LogicalType.Textual, cfg ->
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsNull))
.build();

private final ObjectMapper MAPPER_TO_EMPTY = jsonMapperBuilder()
.withCoercionConfig(LogicalType.Textual, cfg ->
cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsEmpty))
.build();

public void testDefaultIntToStringCoercion() throws JsonProcessingException
{
assertSuccessfulIntToStringCoercionWith(DEFAULT_MAPPER);
}

public void testCoerceConfigToConvert() throws JsonProcessingException
{
assertSuccessfulIntToStringCoercionWith(MAPPER_TRY_CONVERT);
}

public void testCoerceConfigToNull() throws JsonProcessingException
{
assertNull(MAPPER_TO_NULL.readValue("1", String.class));
StringWrapper w = MAPPER_TO_NULL.readValue("{\"str\": -5}", StringWrapper.class);
assertNull(w.str);
String[] arr = MAPPER_TO_NULL.readValue("[ 2 ]", String[].class);
assertEquals(1, arr.length);
assertNull(arr[0]);
}

public void testCoerceConfigToEmpty() throws JsonProcessingException
{
assertEquals("", MAPPER_TO_EMPTY.readValue("3", String.class));
StringWrapper w = MAPPER_TO_EMPTY.readValue("{\"str\": -5}", StringWrapper.class);
assertEquals("", w.str);
String[] arr = MAPPER_TO_EMPTY.readValue("[ 2 ]", String[].class);
assertEquals(1, arr.length);
assertEquals("", arr[0]);
}

public void testCoerceConfigToFail() throws JsonProcessingException
{
_verifyCoerceFail(MAPPER_TO_FAIL, String.class, "3");
_verifyCoerceFail(MAPPER_TO_FAIL, StringWrapper.class, "{\"str\": -5}", "string");
_verifyCoerceFail(MAPPER_TO_FAIL, String[].class, "[ 2 ]", "element of `java.lang.String[]`");
}

/*
/********************************************************
/* Helper methods
/********************************************************
*/

private void assertSuccessfulIntToStringCoercionWith(ObjectMapper objectMapper)
throws JsonProcessingException
{
assertEquals("3", objectMapper.readValue("3", String.class));
assertEquals("-2", objectMapper.readValue("-2", String.class));
{
StringWrapper w = objectMapper.readValue("{\"str\": -5}", StringWrapper.class);
assertEquals("-5", w.str);
String[] arr = objectMapper.readValue("[ 2 ]", String[].class);
assertEquals("2", arr[0]);
}
}

private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType,
String doc) throws JsonProcessingException
{
_verifyCoerceFail(m.reader(), targetType, doc, targetType.getName());
}

private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType,
String doc, String targetTypeDesc) throws JsonProcessingException
{
_verifyCoerceFail(m.reader(), targetType, doc, targetTypeDesc);
}

private void _verifyCoerceFail(ObjectReader r, Class<?> targetType,
String doc, String targetTypeDesc) throws JsonProcessingException
{
try {
r.forType(targetType).readValue(doc);
fail("Should not accept Integer for "+targetType.getName()+" when configured to");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot coerce Integer");
verifyException(e, targetTypeDesc);
}
}
}

0 comments on commit 53f5dda

Please sign in to comment.