Skip to content
Draft
14 changes: 13 additions & 1 deletion src/main/java/tools/jackson/databind/DeserializationFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,19 @@ public enum DeserializationFeature implements ConfigFeature
*<p>
* Feature is enabled by default.
*/
EAGER_DESERIALIZER_FETCH(true)
EAGER_DESERIALIZER_FETCH(true),

/**
* Feature that, when enabled, causes location information to be
* automatically cleared from {@link JacksonException} instances thrown
* during deserialization, preventing potentially sensitive input data
* from appearing in exception messages and logs.
*<p>
* Feature is disabled by default.
*
* @since 3.2
*/
EXCLUDE_LOCATION_IN_EXCEPTIONS(false)

;

Expand Down
70 changes: 46 additions & 24 deletions src/main/java/tools/jackson/databind/ObjectMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -2606,38 +2606,56 @@ protected JsonGenerator _initializeGenerator(JsonGenerator gen) {
/**********************************************************************
*/

/**
* Helper method to clear location from exception if
* {@link DeserializationFeature#EXCLUDE_LOCATION_IN_EXCEPTIONS} is enabled.
*
* @since 3.2
*/
private static <T extends JacksonException> T _clearLocationIfNeeded(
DeserializationConfig config, T e) {
if (config.isEnabled(DeserializationFeature.EXCLUDE_LOCATION_IN_EXCEPTIONS)) {
e.clearLocation();
}
return e;
}

/**
* Actual implementation of value reading+binding operation.
*/
protected Object _readValue(DeserializationContextExt ctxt, JsonParser p,
JavaType valueType)
throws JacksonException
{
// First: may need to read the next token, to initialize
// state (either before first read from parser, or after
// previous token has been cleared)
final Object result;
JsonToken t = _initForReading(p, valueType);

if (t == JsonToken.VALUE_NULL) {
// Ask deserializer what 'null value' to use:
result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else if (t == JsonToken.NOT_AVAILABLE) {
// 28-Jan-2025, tatu: [databind#4932] Need to handle this case too
result = null;
} else { // pointing to event other than null
result = ctxt.readRootValue(p, valueType,
_findRootDeserializer(ctxt, valueType), null);
ctxt.checkUnresolvedObjectId();
}
// Need to consume the token too
p.clearCurrentToken();
if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
_verifyNoTrailingTokens(p, ctxt, valueType);
try {
// First: may need to read the next token, to initialize
// state (either before first read from parser, or after
// previous token has been cleared)
final Object result;
JsonToken t = _initForReading(p, valueType);

if (t == JsonToken.VALUE_NULL) {
// Ask deserializer what 'null value' to use:
result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else if (t == JsonToken.NOT_AVAILABLE) {
// 28-Jan-2025, tatu: [databind#4932] Need to handle this case too
result = null;
} else { // pointing to event other than null
result = ctxt.readRootValue(p, valueType,
_findRootDeserializer(ctxt, valueType), null);
ctxt.checkUnresolvedObjectId();
}
// Need to consume the token too
p.clearCurrentToken();
if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
_verifyNoTrailingTokens(p, ctxt, valueType);
}
return result;
} catch (JacksonException e) {
throw _clearLocationIfNeeded(ctxt.getConfig(), e);
}
return result;
}

protected Object _readMapAndClose(DeserializationContextExt ctxt,
Expand Down Expand Up @@ -2665,6 +2683,8 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt,
_verifyNoTrailingTokens(p, ctxt, valueType);
}
return result;
} catch (JacksonException e) {
throw _clearLocationIfNeeded(ctxt.getConfig(), e);
}
}

Expand Down Expand Up @@ -2703,6 +2723,8 @@ protected JsonNode _readTreeAndClose(DeserializationContextExt ctxt,
_verifyNoTrailingTokens(p, ctxt, valueType);
}
return resultNode;
} catch (JacksonException e) {
throw _clearLocationIfNeeded(ctxt.getConfig(), e);
}
}

Expand Down
71 changes: 45 additions & 26 deletions src/main/java/tools/jackson/databind/ObjectReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -1833,38 +1833,55 @@ public <T> T treeToValue(TreeNode n, JavaType valueType) throws JacksonException
/**********************************************************************
*/

/**
* Helper method to clear location from exception if
* {@link DeserializationFeature#EXCLUDE_LOCATION_IN_EXCEPTIONS} is enabled.
*
* @since 3.2
*/
private <T extends JacksonException> T _clearLocationIfNeeded(T e) {
if (_config.isEnabled(DeserializationFeature.EXCLUDE_LOCATION_IN_EXCEPTIONS)) {
e.clearLocation();
}
return e;
}

/**
* Actual implementation of value reading+binding operation.
*/
protected Object _bind(DeserializationContextExt ctxt,
JsonParser p, Object valueToUpdate) throws JacksonException
{
// First: may need to read the next token, to initialize state (either
// before first read from parser, or after previous token has been cleared)
Object result;
JsonToken t = _initForReading(ctxt, p);
if (t == JsonToken.VALUE_NULL) {
if (valueToUpdate == null) {
result = _findRootDeserializer(ctxt).getNullValue(ctxt);
} else {
try {
// First: may need to read the next token, to initialize state (either
// before first read from parser, or after previous token has been cleared)
Object result;
JsonToken t = _initForReading(ctxt, p);
if (t == JsonToken.VALUE_NULL) {
if (valueToUpdate == null) {
result = _findRootDeserializer(ctxt).getNullValue(ctxt);
} else {
result = valueToUpdate;
}
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = valueToUpdate;
} else if (t == JsonToken.NOT_AVAILABLE) {
// 28-Jan-2025, tatu: [databind#4932] Need to handle this case too
result = valueToUpdate;
} else { // pointing to event other than null
result = ctxt.readRootValue(p, _valueType,
_findRootDeserializer(ctxt), _valueToUpdate);
ctxt.checkUnresolvedObjectId();
}
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = valueToUpdate;
} else if (t == JsonToken.NOT_AVAILABLE) {
// 28-Jan-2025, tatu: [databind#4932] Need to handle this case too
result = valueToUpdate;
} else { // pointing to event other than null
result = ctxt.readRootValue(p, _valueType,
_findRootDeserializer(ctxt), _valueToUpdate);
ctxt.checkUnresolvedObjectId();
}
// Need to consume the token too
p.clearCurrentToken();
if (_config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
_verifyNoTrailingTokens(p, ctxt, _valueType);
// Need to consume the token too
p.clearCurrentToken();
if (_config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
_verifyNoTrailingTokens(p, ctxt, _valueType);
}
return result;
} catch (JacksonException e) {
throw _clearLocationIfNeeded(e);
}
return result;
}

protected Object _bindAndClose(DeserializationContextExt ctxt,
Expand Down Expand Up @@ -1894,6 +1911,8 @@ protected Object _bindAndClose(DeserializationContextExt ctxt,
_verifyNoTrailingTokens(p, ctxt, _valueType);
}
return result;
} catch (JacksonException e) {
throw _clearLocationIfNeeded(e);
}
}

Expand Down Expand Up @@ -1936,7 +1955,7 @@ protected <T> T _collectingBind(DeserializationContextExt ctxt, JsonParser p)
return result;

} catch (DeferredBindingException e) {
throw e; // Already properly formatted
throw _clearLocationIfNeeded(e); // Already properly formatted

} catch (DatabindException e) {
// Hard failure occurred; attach collected problems as suppressed
Expand All @@ -1946,13 +1965,13 @@ protected <T> T _collectingBind(DeserializationContextExt ctxt, JsonParser p)
// Limit was hit - throw DeferredBindingException as primary exception
DeferredBindingException dbe = new DeferredBindingException(p, bucket, true);
dbe.addSuppressed(e); // Original error as suppressed for debugging
throw dbe;
throw _clearLocationIfNeeded(dbe);
} else {
// Hard failure unrelated to limit - keep original as primary
e.addSuppressed(new DeferredBindingException(p, bucket, false));
}
}
throw e;
throw _clearLocationIfNeeded(e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package tools.jackson.databind.exc;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JacksonException;
import tools.jackson.databind.*;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.*;

/**
* Tests for {@link DeserializationFeature#EXCLUDE_LOCATION_IN_EXCEPTIONS}.
*
* @since 3.2
*/
public class ExceptionLocationClearTest extends DatabindTestUtil
{
static class Point {
public int x, y;
}

// By default, exception should include location info
@Test
public void testDefaultBehaviorHasLocation() throws Exception
{
ObjectMapper mapper = newJsonMapper();
try {
mapper.readValue("{ broken }", Point.class);
fail("Should not pass");
} catch (JacksonException e) {
assertNotNull(e.getLocation(), "Location should be present by default");
}
}

// With feature enabled, location should be cleared
@Test
public void testExcludeLocationOnReadValue() throws Exception
{
ObjectMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.EXCLUDE_LOCATION_IN_EXCEPTIONS)
.build();
try {
mapper.readValue("{ broken }", Point.class);
fail("Should not pass");
} catch (JacksonException e) {
assertNull(e.getLocation(),
"Location should be null when EXCLUDE_LOCATION_IN_EXCEPTIONS is enabled");
}
}

// Via ObjectReader
@Test
public void testExcludeLocationViaObjectReader() throws Exception
{
ObjectMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.EXCLUDE_LOCATION_IN_EXCEPTIONS)
.build();
ObjectReader reader = mapper.readerFor(Point.class);
try {
reader.readValue("{ broken }");
fail("Should not pass");
} catch (JacksonException e) {
assertNull(e.getLocation(),
"Location should be null when EXCLUDE_LOCATION_IN_EXCEPTIONS is enabled via ObjectReader");
}
}

// Also test readTree path
@Test
public void testExcludeLocationOnReadTree() throws Exception
{
ObjectMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.EXCLUDE_LOCATION_IN_EXCEPTIONS)
.build();
try {
mapper.readTree("{ broken }");
fail("Should not pass");
} catch (JacksonException e) {
assertNull(e.getLocation(),
"Location should be null when EXCLUDE_LOCATION_IN_EXCEPTIONS is enabled for readTree");
}
}

// Databind-level exception (not just streaming parse error)
@Test
public void testExcludeLocationOnDatabindException() throws Exception
{
ObjectMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.EXCLUDE_LOCATION_IN_EXCEPTIONS)
.build();
try {
// Invalid type coercion should produce a DatabindException
mapper.readValue("\"not a number\"", Point.class);
fail("Should not pass");
} catch (JacksonException e) {
assertNull(e.getLocation(),
"Location should be null for databind-level exceptions too");
}
}
}