FunctionalScalarDeserializer.java
package tools.jackson.databind.deser.std;
import java.util.function.BiFunction;
import java.util.function.Function;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.util.ExceptionUtil;
/**
* A general-purpose deserializer that uses a {@link Function} or {@link BiFunction}
* to convert JSON scalar values (strings, numbers, booleans) into target type instances.
* <p>
* This deserializer is primarily designed for String-based conversions but also
* supports other JSON scalar types via {@code getValueAsString()} coercion.
* Non-scalar JSON values (arrays, objects, embedded objects) are rejected.
* <p>
* <b>Error handling:</b>
* <ul>
* <li>{@link JacksonException} thrown by user code is propagated as-is.</li>
* <li>Other exceptions are wrapped in
* {@link tools.jackson.databind.exc.InvalidFormatException}.</li>
* <li>If {@link tools.jackson.databind.DeserializationFeature#WRAP_EXCEPTIONS}
* is disabled, {@link RuntimeException} is thrown as-is without wrapping.</li>
* </ul>
* <p>
* Usage examples:
* <pre>
* // Simple case - method reference
* new FunctionalScalarDeserializer<>(Bar.class, Bar::of)
*
* // Full access case
* new FunctionalScalarDeserializer<>(Bar.class, (p, ctx) ->
* Bar.parse(p.getValueAsString(), ctx.getLocale()))
* </pre>
*
* @param <T> Target type to deserialize into
*
* @since 3.1
*/
public class FunctionalScalarDeserializer<T> extends StdScalarDeserializer<T>
{
protected final BiFunction<JsonParser, DeserializationContext, T> _biFunction;
protected final Function<String, T> _stringFunction;
/*
/**********************************************************************
/* Life-cycle
/**********************************************************************
*/
public FunctionalScalarDeserializer(Class<T> type,
BiFunction<JsonParser, DeserializationContext, T> function) {
super(type);
_biFunction = function;
_stringFunction = null;
}
public FunctionalScalarDeserializer(JavaType type,
BiFunction<JsonParser, DeserializationContext, T> function) {
super(type);
_biFunction = function;
_stringFunction = null;
}
public FunctionalScalarDeserializer(Class<T> type, Function<String, T> function) {
super(type);
_biFunction = null;
_stringFunction = function;
}
public FunctionalScalarDeserializer(JavaType type, Function<String, T> function) {
super(type);
_biFunction = null;
_stringFunction = function;
}
@Override
public LogicalType logicalType() {
return LogicalType.OtherScalar;
}
/*
/**********************************************************************
/* Deserializer implementations
/**********************************************************************
*/
@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
// BiFunction: invoke directly without any pre-processing
if (_biFunction != null) {
try {
return _biFunction.apply(p, ctxt);
} catch (Exception e) {
return _handleException(p, ctxt, e);
}
}
// Function<String, T>: extract text and pass to function
String text = p.getValueAsString();
if (text == null) {
JsonToken t = p.currentToken();
if (t == JsonToken.START_OBJECT) {
text = ctxt.extractScalarFromObject(p, this, _valueClass);
if (text == null) {
return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
}
} else {
// Non-scalar tokens (arrays, embedded objects, etc.) are not supported
return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
}
}
if (text.isEmpty()) {
return (T) _deserializeFromEmptyString(ctxt);
}
try {
return _stringFunction.apply(text);
} catch (Exception e) {
return _handleException(text, ctxt, e);
}
}
private T _handleException(JsonParser p, DeserializationContext ctxt,
Exception e)
throws JacksonException
{
if (e instanceof JacksonException je) {
throw je;
}
return _handleException(p.getValueAsString(), ctxt, e);
}
private T _handleException(String text, DeserializationContext ctxt, Exception e)
throws JacksonException
{
e = ExceptionUtil.rethrowIfNoWrap(ctxt, e);
String msg = "not a valid textual representation";
String m2 = e.getMessage();
if (m2 != null) {
msg = msg + ", problem: " + m2;
}
throw ctxt.weirdStringException(text, _valueClass, msg)
.withCause(e);
}
/**
* Handle empty String input according to {@link CoercionAction} configuration.
*/
private Object _deserializeFromEmptyString(DeserializationContext ctxt)
throws JacksonException
{
CoercionAction act = ctxt.findCoercionAction(logicalType(), _valueClass,
CoercionInputShape.EmptyString);
if (act == CoercionAction.Fail) {
ctxt.reportInputMismatch(this,
"Cannot coerce empty String (\"\") to %s (but could if enabling coercion using `CoercionConfig`)",
_coercedTypeDesc());
}
if (act == CoercionAction.AsEmpty) {
return getEmptyValue(ctxt);
}
// if (act == CoercionAction.AsNull) etc
return getNullValue(ctxt);
}
}