diff --git a/README.md b/README.md
index 0d81ec6ca3..b995a9884f 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/release-notes/VERSION b/release-notes/VERSION
index 3a7410c418..2f4e52c105 100644
--- a/release-notes/VERSION
+++ b/release-notes/VERSION
@@ -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
diff --git a/src/main/java/tools/jackson/databind/ObjectReader.java b/src/main/java/tools/jackson/databind/ObjectReader.java
index 1346bdf0df..0ea174a961 100644
--- a/src/main/java/tools/jackson/databind/ObjectReader.java
+++ b/src/main/java/tools/jackson/databind/ObjectReader.java
@@ -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;
@@ -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).
+ *
+ *
IMPORTANT: This method registers a {@link CollectingProblemHandler} which
+ * replaces any previously configured {@link DeserializationProblemHandler}.
+ * If you need custom problem handling in addition to collection, you must implement
+ * your own handler that delegates to {@code CollectingProblemHandler} or chain handlers.
+ *
+ *
Future versions may support handler chaining; for now, only one handler is active.
+ *
+ *
Thread-safety: The returned reader is immutable and thread-safe. Each call to
+ * {@link #readValueCollectingProblems} allocates a fresh problem bucket, so concurrent
+ * calls do not interfere.
+ *
+ *
+ *
+ * @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.
+ *
+ *
Thread-safety: The returned reader is immutable and thread-safe.
+ * 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
+ */
+ 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));
}
@@ -1320,6 +1385,207 @@ public 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.
+ *
+ *
Usage: 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.
+ *
+ *
Error handling:
+ *
+ *
Recoverable errors are accumulated and thrown as
+ * {@link DeferredBindingException} after parsing
+ *
Hard (non-recoverable) failures throw immediately, with collected problems
+ * attached as suppressed exceptions
+ *
When the configured limit is reached, collection stops
+ *
+ *
+ *
Exception Handling Strategy:
+ *
+ *
This method catches only {@link DatabindException} subtypes (not all
+ * {@link JacksonException}s) because:
+ *
+ *
+ *
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)
+ *
+ *
Only databind-level errors (type conversion, unknown properties, instantiation
+ * failures) are potentially recoverable and suitable for collection
+ *
+ *
Catching all JacksonExceptions would hide critical parsing errors that should
+ * fail fast
+ *
+ *
+ *
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.
+ *
+ *
Thread-safety: Each call allocates a fresh problem bucket,
+ * so multiple concurrent calls on the same reader instance are safe.
+ *
+ *
Parser filtering: Unlike convenience overloads ({@link #readValueCollectingProblems(String)},
+ * {@link #readValueCollectingProblems(byte[])}, etc.), this method does not apply
+ * parser filtering. Callers are responsible for filter wrapping if needed.
+ *
+ * @param 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 readValueCollectingProblems(JsonParser p) throws JacksonException {
+ _assertNotNull("p", p);
+
+ // CRITICAL: Allocate a FRESH bucket for THIS call (thread-safety)
+ List 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) {
+ // 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 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 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 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 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 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")
diff --git a/src/main/java/tools/jackson/databind/deser/CollectingProblemHandler.java b/src/main/java/tools/jackson/databind/deser/CollectingProblemHandler.java
new file mode 100644
index 0000000000..585b00e3be
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/deser/CollectingProblemHandler.java
@@ -0,0 +1,367 @@
+package tools.jackson.databind.deser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonPointer;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.TokenStreamContext;
+import tools.jackson.core.TokenStreamLocation;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.exc.CollectedProblem;
+
+/**
+ * Stateless {@link DeserializationProblemHandler} that collects recoverable
+ * problems into a per-call bucket stored in {@link DeserializationContext}
+ * attributes.
+ *
+ *
Design: This handler is completely stateless. The problem collection
+ * bucket is allocated per-call by
+ * {@link tools.jackson.databind.ObjectReader#readValueCollectingProblems ObjectReader.readValueCollectingProblems(...)}
+ * and stored in per-call {@link tools.jackson.databind.cfg.ContextAttributes ContextAttributes},
+ * ensuring thread-safety and call isolation.
+ *
+ *
Usage: This class is internal infrastructure, registered automatically by
+ * {@link tools.jackson.databind.ObjectReader#problemCollectingReader() ObjectReader.problemCollectingReader()}.
+ * Users should not instantiate or register this handler manually.
+ *
+ *
Problem collection state is stored in {@link DeserializationContext} attributes
+ * rather than within this handler for several reasons:
+ *
+ *
+ *
Thread-safety: The handler instance is shared across all calls to the
+ * same ObjectReader. Storing mutable state in the handler would require
+ * synchronization and complicate the implementation.
+ *
+ *
Call isolation: Each call to {@code readValueCollectingProblems()} needs
+ * its own problem bucket. Context attributes are perfect for this - they're created
+ * per-call and automatically cleaned up after deserialization.
+ *
+ *
Immutability: Jackson's config objects (including handlers) are designed
+ * to be immutable and reusable. Storing per-call state violates this principle.
+ *
+ *
Configuration vs State: The handler stores configuration (max problems
+ * limit) while attributes store runtime state (the actual problem list). This
+ * separation follows Jackson's design patterns.
+ *
+ *
+ *
The handler itself is stateless - it's just a strategy for handling problems.
+ * The actual collection happens in a bucket passed through context attributes.
+ *
+ *
Recoverable errors handled:
+ *
+ *
Unknown properties ({@link #handleUnknownProperty handleUnknownProperty}) - skips children