Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
}
}
}