Skip to content
Open
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
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,60 @@ This will deserialize JSON fields with `known_as`, as well as `identifer` and `f
Note: to use the `@JsonAlias` annotation, a `@JsonProperty` annotation must also be used.

Overall, Jackson library is very powerful in deserializing objects using builder pattern.


## Tutorial: Collecting multiple errors (3.1+)

One recently introduced feature is the ability to collect multiple deserialization errors instead of failing fast on the first one. This can be really handy for validation use cases.

By default, if Jackson encounters a problem during deserialization -- say, string `"xyz"` for an `int` property -- it will immediately throw an exception and stop. But sometimes you want to see ALL the problems in one go.

Consider a case where you have a couple of fields with bad data:

```java
class Order {
public int orderId;
public Date orderDate;
public double amount;
}

String json = "{\"orderId\":\"not-a-number\",\"orderDate\":\"bad-date\",\"amount\":\"xyz\"}";
```

Normally you'd get an error about `orderId`, fix it, resubmit, then get error about `orderDate`, and so on. Not fun. So let's collect them all:

```java
ObjectMapper mapper = new JsonMapper();
ObjectReader reader = mapper.readerFor(Order.class).problemCollectingReader();

try {
Order result = reader.readValueCollectingProblems(json);
// worked fine
} catch (DeferredBindingException ex) {
System.out.println("Found " + ex.getProblems().size() + " problems:");
for (CollectedProblem problem : ex.getProblems()) {
System.out.println(problem.getPath() + ": " + problem.getMessage());
// Can also access problem.getRawValue() to see what the bad input was
}
}
```

This will report all 3 problems at once. Much better.

By default, Jackson will collect up to 100 problems before giving up (to prevent DoS-style attacks with huge bad payloads). You can configure this:

```java
ObjectReader reader = mapper.readerFor(Order.class).problemCollectingReader(10); // limit to 10
```

Few things to keep in mind:

1. This is best-effort: not all problems can be collected. Malformed JSON (like missing closing brace) or other structural problems will still fail immediately. But type conversion errors, unknown properties (if you enable that check), and such will be collected.
2. Error paths use JSON Pointer notation (RFC 6901): so `"/items/0/price"` means first item in `items` array, `price` field. Special characters get escaped (`~` becomes `~0`, `/` becomes `~1`).
3. Each call to `readValueCollectingProblems()` gets its own problem bucket, so it's thread-safe to reuse the same `ObjectReader`.
4. Fields that fail to deserialize get default values (0 for primitives, null for objects), so you do get a result object back (thrown in the exception).

This is particularly useful for things like REST API validation (return all validation errors to client), or batch processing (log errors but keep going), or development tooling.

# Contribute!

We would love to get your contribution, whether it's in form of bug reports, Requests for Enhancement (RFE), documentation, or code patches.
Expand Down
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Versions: 3.x (for earlier see VERSION-2.x)

3.1.0 (not yet released)

#1196: Add opt-in error collection for deserialization
(requested by @odrotbohm, contributed by @sri-adarsh-kumar)
#5350: Add `DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES` for
selecting `null` vs "empty/absent" value when deserializing missing `Optional` value

Expand Down
266 changes: 266 additions & 0 deletions src/main/java/tools/jackson/databind/ObjectReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
import tools.jackson.databind.cfg.ContextAttributes;
import tools.jackson.databind.cfg.DatatypeFeature;
import tools.jackson.databind.cfg.DeserializationContexts;
import tools.jackson.databind.deser.CollectingProblemHandler;
import tools.jackson.databind.deser.DeserializationContextExt;
import tools.jackson.databind.deser.DeserializationProblemHandler;
import tools.jackson.databind.exc.CollectedProblem;
import tools.jackson.databind.exc.DeferredBindingException;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
Expand Down Expand Up @@ -692,6 +695,68 @@ public ObjectReader withHandler(DeserializationProblemHandler h) {
return _with(_config.withHandler(h));
}

/**
* Returns a new {@link ObjectReader} configured to collect deserialization problems
* instead of failing on the first error. Uses default problem limit (100 problems).
*
* <p><b>IMPORTANT</b>: This method registers a {@link CollectingProblemHandler} which
* <b>replaces any previously configured {@link DeserializationProblemHandler}</b>.
* If you need custom problem handling in addition to collection, you must implement
* your own handler that delegates to {@code CollectingProblemHandler} or chain handlers.
*
* <p>Future versions may support handler chaining; for now, only one handler is active.
*
* <p><b>Thread-safety</b>: The returned reader is immutable and thread-safe. Each call to
* {@link #readValueCollectingProblems} allocates a fresh problem bucket, so concurrent
* calls do not interfere.
*
* <p>Usage:
* <pre>
* ObjectReader reader = mapper.reader()
* .forType(MyBean.class)
* .problemCollectingReader();
*
* MyBean bean = reader.readValueCollectingProblems(json);
* </pre>
*
* @return A new ObjectReader configured for problem collection
* @since 3.1
*/
public ObjectReader problemCollectingReader() {
return problemCollectingReader(100); // Default limit
}

/**
* Enables problem collection mode with a custom problem limit.
*
* <p><b>Thread-safety</b>: The returned reader is immutable and thread-safe.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to explain that this is implemented as a problem handler, replacing any handler that might have been formerly configured.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in the Javadoc for problemCollectingReader method.

* Each call to {@link #readValueCollectingProblems} allocates a fresh problem bucket,
* so concurrent calls do not interfere.
*
* @param maxProblems Maximum number of problems to collect (must be > 0)
* @return A new ObjectReader configured for problem collection
* @throws IllegalArgumentException if maxProblems is <= 0
* @since 3.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the throws should be included in the javadoc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added now. Thanks for the feedback.

*/
public ObjectReader problemCollectingReader(int maxProblems) {
if (maxProblems <= 0) {
throw new IllegalArgumentException("maxProblems must be positive");
}

// Store ONLY the max limit in config (not the bucket)
// Bucket will be allocated fresh per-call in readValueCollectingProblems()
ContextAttributes attrs = _config.getAttributes()
.withSharedAttribute(CollectingProblemHandler.ATTR_MAX_PROBLEMS, maxProblems);

DeserializationConfig newConfig = _config
.withHandler(new CollectingProblemHandler())
.with(attrs);

// Return new immutable reader (no mutable state)
return _new(this, newConfig, _valueType, _rootDeserializer, _valueToUpdate,
_schema, _injectableValues);
}

public ObjectReader with(Base64Variant defaultBase64) {
return _with(_config.with(defaultBase64));
}
Expand Down Expand Up @@ -1320,6 +1385,207 @@ public <T> T readValue(TokenBuffer src) throws JacksonException
_considerFilter(src.asParser(ctxt) , false));
}

/*
/**********************************************************************
/* Deserialization methods with error collection
/**********************************************************************
*/

/**
* Deserializes JSON content into a Java object, collecting multiple
* problems if encountered. If any problems were collected, throws
* {@link DeferredBindingException} with all problems.
*
* <p><b>Usage</b>: This method should be called on an ObjectReader created via
* {@link #problemCollectingReader()} or {@link #problemCollectingReader(int)}. If called on a regular
* reader (without problem collection enabled), it behaves the same as
* {@link #readValue(JsonParser)} since no handler is registered.
*
* <p><b>Error handling</b>:
* <ul>
* <li>Recoverable errors are accumulated and thrown as
* {@link DeferredBindingException} after parsing</li>
* <li>Hard (non-recoverable) failures throw immediately, with collected problems
* attached as suppressed exceptions</li>
* <li>When the configured limit is reached, collection stops</li>
* </ul>
*
* <p><b>Exception Handling Strategy</b>:
*
* <p>This method catches only {@link DatabindException} subtypes (not all
* {@link JacksonException}s) because:
*
* <ul>
* <li>Core streaming errors ({@link tools.jackson.core.exc.StreamReadException},
* {@link tools.jackson.core.exc.StreamWriteException}) represent structural
* JSON problems that cannot be recovered from (malformed JSON, I/O errors)</li>
*
* <li>Only databind-level errors (type conversion, unknown properties, instantiation
* failures) are potentially recoverable and suitable for collection</li>
*
* <li>Catching all JacksonExceptions would hide critical parsing errors that should
* fail fast</li>
* </ul>
*
* <p>If a hard failure occurs after some problems have been collected, those problems
* are attached as suppressed exceptions to the thrown exception for debugging purposes.
*
* <p><b>Thread-safety</b>: Each call allocates a fresh problem bucket,
* so multiple concurrent calls on the same reader instance are safe.
*
* <p><b>Parser filtering</b>: Unlike convenience overloads ({@link #readValueCollectingProblems(String)},
* {@link #readValueCollectingProblems(byte[])}, etc.), this method does <i>not</i> apply
* parser filtering. Callers are responsible for filter wrapping if needed.
*
* @param <T> Type to deserialize
* @param p JsonParser to read from (will not be closed by this method)
* @return Deserialized object
* @throws DeferredBindingException if recoverable problems were collected
* @throws DatabindException if a non-recoverable error occurred
* @since 3.1
*/
public <T> T readValueCollectingProblems(JsonParser p) throws JacksonException {
_assertNotNull("p", p);

// CRITICAL: Allocate a FRESH bucket for THIS call (thread-safety)
List<CollectedProblem> bucket = new ArrayList<>();

// Create per-call attributes with the fresh bucket
ContextAttributes perCallAttrs = _config.getAttributes()
.withPerCallAttribute(CollectingProblemHandler.class, bucket);

// Create a temporary ObjectReader with per-call attributes using public API
ObjectReader perCallReader = this.with(perCallAttrs);

try {
// Delegate to the temporary reader's existing readValue method
T result = perCallReader.readValue(p);

// Check if any problems were collected
if (!bucket.isEmpty()) {
// Check if limit was reached - read from per-call config to honor overrides
Integer maxProblems = (Integer) perCallReader.getConfig().getAttributes()
.getAttribute(CollectingProblemHandler.ATTR_MAX_PROBLEMS);
boolean limitReached = (maxProblems != null &&
bucket.size() >= maxProblems);

throw new DeferredBindingException(p, bucket, limitReached);
}

return result;

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

} catch (DatabindException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the idea to only catch DatabindExceptions and not other JacksonExceptions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is intentional. DatabindException represents deserialization/binding errors that we want to collect (missing properties, type mismatches, etc.).

Other Jackson exceptions (like JsonParseException, StreamReadException) might pertain more to malformed JSON structure that would fail fast and not be collected.

// Hard failure occurred; attach collected problems as suppressed
if (!bucket.isEmpty()) {
// Read from per-call config to honor overrides
Integer maxProblems = (Integer) perCallReader.getConfig().getAttributes()
.getAttribute(CollectingProblemHandler.ATTR_MAX_PROBLEMS);
boolean limitReached = (maxProblems != null &&
bucket.size() >= maxProblems);

e.addSuppressed(new DeferredBindingException(p, bucket, limitReached));
}
throw e;
}
}

/**
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
*/
public <T> T readValueCollectingProblems(String content) throws JacksonException {
_assertNotNull("content", content);
DeserializationContextExt ctxt = _deserializationContext();
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, content), true);
try {
return readValueCollectingProblems(p);
} finally {
try {
p.close();
} catch (Exception e) {
// ignore
}
}
}

/**
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
*/
@SuppressWarnings("unchecked")
public <T> T readValueCollectingProblems(byte[] content) throws JacksonException {
_assertNotNull("content", content);
DeserializationContextExt ctxt = _deserializationContext();
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, content), true);
try {
return readValueCollectingProblems(p);
} finally {
try {
p.close();
} catch (Exception e) {
// ignore
}
}
}

/**
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
*/
@SuppressWarnings("unchecked")
public <T> T readValueCollectingProblems(File src) throws JacksonException {
_assertNotNull("src", src);
DeserializationContextExt ctxt = _deserializationContext();
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, src), true);
try {
return readValueCollectingProblems(p);
} finally {
try {
p.close();
} catch (Exception e) {
// ignore
}
}
}

/**
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
*/
@SuppressWarnings("unchecked")
public <T> T readValueCollectingProblems(InputStream src) throws JacksonException {
_assertNotNull("src", src);
DeserializationContextExt ctxt = _deserializationContext();
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, src), true);
try {
return readValueCollectingProblems(p);
} finally {
try {
p.close();
} catch (Exception e) {
// ignore
}
}
}

/**
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
*/
@SuppressWarnings("unchecked")
public <T> T readValueCollectingProblems(Reader src) throws JacksonException {
_assertNotNull("src", src);
DeserializationContextExt ctxt = _deserializationContext();
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, src), true);
try {
return readValueCollectingProblems(p);
} finally {
try {
p.close();
} catch (Exception e) {
// ignore
}
}
}

/*
/**********************************************************************
/* Deserialization methods; JsonNode ("tree")
Expand Down
Loading