Skip to content

Commit f0c4342

Browse files
Address PR #5364 review feedback: rename API and improve documentation
This commit addresses all technical feedback from reviewers @pjfanning, @cowtowncoder, and @JooHyukKim on PR #5364. API Changes: - Rename collectErrors() → problemCollectingReader() - Rename readValueCollecting() → readValueCollectingProblems() - Rationale: "Error" conflicts with Java's Error class (OutOfMemoryError). "Problem" aligns with existing DeserializationProblemHandler terminology. Code Quality: - Add proper imports for CollectingProblemHandler, CollectedProblem, DeferredBindingException - Replace 6 fully qualified class names with short names - Add missing @throws javadoc tags Remove Code Duplication: - Refactor buildJsonPointer() to use jackson-core's JsonPointer API (appendProperty/appendIndex methods) - Delete custom escapeJsonPointerSegment() method (~8 lines) - Leverage tested jackson-core RFC 6901 escaping implementation Enhanced Documentation: - Add architecture rationale to CollectingProblemHandler explaining why context attributes are used (thread-safety, call isolation, immutability, config vs state separation) - Add exception handling strategy to readValueCollectingProblems() explaining why only DatabindException is caught (not all JacksonExceptions - streaming errors should fail fast) - Add handler replacement warning to problemCollectingReader() - Update DeferredBindingException javadoc with new API names Files changed: 5 - README.md (tutorial examples) - ObjectReader.java (main API) - CollectingProblemHandler.java (implementation) - DeferredBindingException.java (exception javadoc) - CollectingErrorsTest.java (test updates) All tests pass. No behavioral changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 24604ba commit f0c4342

File tree

5 files changed

+187
-144
lines changed

5 files changed

+187
-144
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -604,10 +604,10 @@ Normally you'd get an error about `orderId`, fix it, resubmit, then get error ab
604604

605605
```java
606606
ObjectMapper mapper = new JsonMapper();
607-
ObjectReader reader = mapper.readerFor(Order.class).collectErrors();
607+
ObjectReader reader = mapper.readerFor(Order.class).problemCollectingReader();
608608

609609
try {
610-
Order result = reader.readValueCollecting(json);
610+
Order result = reader.readValueCollectingProblems(json);
611611
// worked fine
612612
} catch (DeferredBindingException ex) {
613613
System.out.println("Found " + ex.getProblems().size() + " problems:");
@@ -620,17 +620,17 @@ try {
620620

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

623-
By default, Jackson will collect up to 100 errors before giving up (to prevent DoS-style attacks with huge bad payloads). You can configure this:
623+
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:
624624

625625
```java
626-
ObjectReader reader = mapper.readerFor(Order.class).collectErrors(10); // limit to 10
626+
ObjectReader reader = mapper.readerFor(Order.class).problemCollectingReader(10); // limit to 10
627627
```
628628

629629
Few things to keep in mind:
630630

631-
1. This is best-effort: not all errors 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.
631+
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.
632632
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`).
633-
3. Each call to `readValueCollecting()` gets its own error bucket, so it's thread-safe to reuse the same `ObjectReader`.
633+
3. Each call to `readValueCollectingProblems()` gets its own problem bucket, so it's thread-safe to reuse the same `ObjectReader`.
634634
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).
635635

636636
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.

src/main/java/tools/jackson/databind/ObjectReader.java

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
import tools.jackson.databind.cfg.ContextAttributes;
1818
import tools.jackson.databind.cfg.DatatypeFeature;
1919
import tools.jackson.databind.cfg.DeserializationContexts;
20+
import tools.jackson.databind.deser.CollectingProblemHandler;
2021
import tools.jackson.databind.deser.DeserializationContextExt;
2122
import tools.jackson.databind.deser.DeserializationProblemHandler;
23+
import tools.jackson.databind.exc.CollectedProblem;
24+
import tools.jackson.databind.exc.DeferredBindingException;
2225
import tools.jackson.databind.node.ArrayNode;
2326
import tools.jackson.databind.node.JsonNodeFactory;
2427
import tools.jackson.databind.node.ObjectNode;
@@ -693,53 +696,60 @@ public ObjectReader withHandler(DeserializationProblemHandler h) {
693696
}
694697

695698
/**
696-
* Enables error collection mode by registering a
697-
* {@link tools.jackson.databind.deser.CollectingProblemHandler} with default
698-
* error limit (100 problems).
699+
* Returns a new {@link ObjectReader} configured to collect deserialization problems
700+
* instead of failing on the first error. Uses default problem limit (100 problems).
699701
*
700-
* <p>The returned reader is immutable and thread-safe. Each call to
701-
* {@link #readValueCollecting} allocates a fresh problem bucket, so concurrent
702+
* <p><b>IMPORTANT</b>: This method registers a {@link CollectingProblemHandler} which
703+
* <b>replaces any previously configured {@link DeserializationProblemHandler}</b>.
704+
* If you need custom problem handling in addition to collection, you must implement
705+
* your own handler that delegates to {@code CollectingProblemHandler} or chain handlers.
706+
*
707+
* <p>Future versions may support handler chaining; for now, only one handler is active.
708+
*
709+
* <p><b>Thread-safety</b>: The returned reader is immutable and thread-safe. Each call to
710+
* {@link #readValueCollectingProblems} allocates a fresh problem bucket, so concurrent
702711
* calls do not interfere.
703712
*
704713
* <p>Usage:
705714
* <pre>
706715
* ObjectReader reader = mapper.reader()
707716
* .forType(MyBean.class)
708-
* .collectErrors();
717+
* .problemCollectingReader();
709718
*
710-
* MyBean bean = reader.readValueCollecting(json);
719+
* MyBean bean = reader.readValueCollectingProblems(json);
711720
* </pre>
712721
*
713-
* @return A new ObjectReader configured for error collection
722+
* @return A new ObjectReader configured for problem collection
714723
* @since 3.1
715724
*/
716-
public ObjectReader collectErrors() {
717-
return collectErrors(100); // Default limit
725+
public ObjectReader problemCollectingReader() {
726+
return problemCollectingReader(100); // Default limit
718727
}
719728

720729
/**
721-
* Enables error collection mode with a custom problem limit.
730+
* Enables problem collection mode with a custom problem limit.
722731
*
723732
* <p><b>Thread-safety</b>: The returned reader is immutable and thread-safe.
724-
* Each call to {@link #readValueCollecting} allocates a fresh problem bucket,
733+
* Each call to {@link #readValueCollectingProblems} allocates a fresh problem bucket,
725734
* so concurrent calls do not interfere.
726735
*
727736
* @param maxProblems Maximum number of problems to collect (must be > 0)
728-
* @return A new ObjectReader configured for error collection
737+
* @return A new ObjectReader configured for problem collection
738+
* @throws IllegalArgumentException if maxProblems is <= 0
729739
* @since 3.1
730740
*/
731-
public ObjectReader collectErrors(int maxProblems) {
741+
public ObjectReader problemCollectingReader(int maxProblems) {
732742
if (maxProblems <= 0) {
733743
throw new IllegalArgumentException("maxProblems must be positive");
734744
}
735745

736746
// Store ONLY the max limit in config (not the bucket)
737-
// Bucket will be allocated fresh per-call in readValueCollecting()
747+
// Bucket will be allocated fresh per-call in readValueCollectingProblems()
738748
ContextAttributes attrs = _config.getAttributes()
739-
.withSharedAttribute(tools.jackson.databind.deser.CollectingProblemHandler.ATTR_MAX_PROBLEMS, maxProblems);
749+
.withSharedAttribute(CollectingProblemHandler.ATTR_MAX_PROBLEMS, maxProblems);
740750

741751
DeserializationConfig newConfig = _config
742-
.withHandler(new tools.jackson.databind.deser.CollectingProblemHandler())
752+
.withHandler(new CollectingProblemHandler())
743753
.with(attrs);
744754

745755
// Return new immutable reader (no mutable state)
@@ -1383,46 +1393,66 @@ public <T> T readValue(TokenBuffer src) throws JacksonException
13831393

13841394
/**
13851395
* Deserializes JSON content into a Java object, collecting multiple
1386-
* errors if encountered. If any problems were collected, throws
1387-
* {@link tools.jackson.databind.exc.DeferredBindingException} with all problems.
1396+
* problems if encountered. If any problems were collected, throws
1397+
* {@link DeferredBindingException} with all problems.
13881398
*
13891399
* <p><b>Usage</b>: This method should be called on an ObjectReader created via
1390-
* {@link #collectErrors()} or {@link #collectErrors(int)}. If called on a regular
1391-
* reader (without error collection enabled), it behaves the same as
1400+
* {@link #problemCollectingReader()} or {@link #problemCollectingReader(int)}. If called on a regular
1401+
* reader (without problem collection enabled), it behaves the same as
13921402
* {@link #readValue(JsonParser)} since no handler is registered.
13931403
*
13941404
* <p><b>Error handling</b>:
13951405
* <ul>
13961406
* <li>Recoverable errors are accumulated and thrown as
1397-
* {@link tools.jackson.databind.exc.DeferredBindingException} after parsing</li>
1407+
* {@link DeferredBindingException} after parsing</li>
13981408
* <li>Hard (non-recoverable) failures throw immediately, with collected problems
13991409
* attached as suppressed exceptions</li>
14001410
* <li>When the configured limit is reached, collection stops</li>
14011411
* </ul>
14021412
*
1413+
* <p><b>Exception Handling Strategy</b>:
1414+
*
1415+
* <p>This method catches only {@link DatabindException} subtypes (not all
1416+
* {@link JacksonException}s) because:
1417+
*
1418+
* <ul>
1419+
* <li>Core streaming errors ({@link tools.jackson.core.exc.StreamReadException},
1420+
* {@link tools.jackson.core.exc.StreamWriteException}) represent structural
1421+
* JSON problems that cannot be recovered from (malformed JSON, I/O errors)</li>
1422+
*
1423+
* <li>Only databind-level errors (type conversion, unknown properties, instantiation
1424+
* failures) are potentially recoverable and suitable for collection</li>
1425+
*
1426+
* <li>Catching all JacksonExceptions would hide critical parsing errors that should
1427+
* fail fast</li>
1428+
* </ul>
1429+
*
1430+
* <p>If a hard failure occurs after some problems have been collected, those problems
1431+
* are attached as suppressed exceptions to the thrown exception for debugging purposes.
1432+
*
14031433
* <p><b>Thread-safety</b>: Each call allocates a fresh problem bucket,
14041434
* so multiple concurrent calls on the same reader instance are safe.
14051435
*
1406-
* <p><b>Parser filtering</b>: Unlike convenience overloads ({@link #readValueCollecting(String)},
1407-
* {@link #readValueCollecting(byte[])}, etc.), this method does <i>not</i> apply
1436+
* <p><b>Parser filtering</b>: Unlike convenience overloads ({@link #readValueCollectingProblems(String)},
1437+
* {@link #readValueCollectingProblems(byte[])}, etc.), this method does <i>not</i> apply
14081438
* parser filtering. Callers are responsible for filter wrapping if needed.
14091439
*
14101440
* @param <T> Type to deserialize
14111441
* @param p JsonParser to read from (will not be closed by this method)
14121442
* @return Deserialized object
1413-
* @throws tools.jackson.databind.exc.DeferredBindingException if recoverable problems were collected
1414-
* @throws tools.jackson.databind.DatabindException if a non-recoverable error occurred
1443+
* @throws DeferredBindingException if recoverable problems were collected
1444+
* @throws DatabindException if a non-recoverable error occurred
14151445
* @since 3.1
14161446
*/
1417-
public <T> T readValueCollecting(JsonParser p) throws JacksonException {
1447+
public <T> T readValueCollectingProblems(JsonParser p) throws JacksonException {
14181448
_assertNotNull("p", p);
14191449

14201450
// CRITICAL: Allocate a FRESH bucket for THIS call (thread-safety)
1421-
List<tools.jackson.databind.exc.CollectedProblem> bucket = new ArrayList<>();
1451+
List<CollectedProblem> bucket = new ArrayList<>();
14221452

14231453
// Create per-call attributes with the fresh bucket
14241454
ContextAttributes perCallAttrs = _config.getAttributes()
1425-
.withPerCallAttribute(tools.jackson.databind.deser.CollectingProblemHandler.class, bucket);
1455+
.withPerCallAttribute(CollectingProblemHandler.class, bucket);
14261456

14271457
// Create a temporary ObjectReader with per-call attributes using public API
14281458
ObjectReader perCallReader = this.with(perCallAttrs);
@@ -1435,42 +1465,42 @@ public <T> T readValueCollecting(JsonParser p) throws JacksonException {
14351465
if (!bucket.isEmpty()) {
14361466
// Check if limit was reached - read from per-call config to honor overrides
14371467
Integer maxProblems = (Integer) perCallReader.getConfig().getAttributes()
1438-
.getAttribute(tools.jackson.databind.deser.CollectingProblemHandler.ATTR_MAX_PROBLEMS);
1468+
.getAttribute(CollectingProblemHandler.ATTR_MAX_PROBLEMS);
14391469
boolean limitReached = (maxProblems != null &&
14401470
bucket.size() >= maxProblems);
14411471

1442-
throw new tools.jackson.databind.exc.DeferredBindingException(p, bucket, limitReached);
1472+
throw new DeferredBindingException(p, bucket, limitReached);
14431473
}
14441474

14451475
return result;
14461476

1447-
} catch (tools.jackson.databind.exc.DeferredBindingException e) {
1477+
} catch (DeferredBindingException e) {
14481478
throw e; // Already properly formatted
14491479

14501480
} catch (DatabindException e) {
14511481
// Hard failure occurred; attach collected problems as suppressed
14521482
if (!bucket.isEmpty()) {
14531483
// Read from per-call config to honor overrides
14541484
Integer maxProblems = (Integer) perCallReader.getConfig().getAttributes()
1455-
.getAttribute(tools.jackson.databind.deser.CollectingProblemHandler.ATTR_MAX_PROBLEMS);
1485+
.getAttribute(CollectingProblemHandler.ATTR_MAX_PROBLEMS);
14561486
boolean limitReached = (maxProblems != null &&
14571487
bucket.size() >= maxProblems);
14581488

1459-
e.addSuppressed(new tools.jackson.databind.exc.DeferredBindingException(p, bucket, limitReached));
1489+
e.addSuppressed(new DeferredBindingException(p, bucket, limitReached));
14601490
}
14611491
throw e;
14621492
}
14631493
}
14641494

14651495
/**
1466-
* Convenience overload for {@link #readValueCollecting(JsonParser)}.
1496+
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
14671497
*/
1468-
public <T> T readValueCollecting(String content) throws JacksonException {
1498+
public <T> T readValueCollectingProblems(String content) throws JacksonException {
14691499
_assertNotNull("content", content);
14701500
DeserializationContextExt ctxt = _deserializationContext();
14711501
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, content), true);
14721502
try {
1473-
return readValueCollecting(p);
1503+
return readValueCollectingProblems(p);
14741504
} finally {
14751505
try {
14761506
p.close();
@@ -1481,15 +1511,15 @@ public <T> T readValueCollecting(String content) throws JacksonException {
14811511
}
14821512

14831513
/**
1484-
* Convenience overload for {@link #readValueCollecting(JsonParser)}.
1514+
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
14851515
*/
14861516
@SuppressWarnings("unchecked")
1487-
public <T> T readValueCollecting(byte[] content) throws JacksonException {
1517+
public <T> T readValueCollectingProblems(byte[] content) throws JacksonException {
14881518
_assertNotNull("content", content);
14891519
DeserializationContextExt ctxt = _deserializationContext();
14901520
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, content), true);
14911521
try {
1492-
return readValueCollecting(p);
1522+
return readValueCollectingProblems(p);
14931523
} finally {
14941524
try {
14951525
p.close();
@@ -1500,15 +1530,15 @@ public <T> T readValueCollecting(byte[] content) throws JacksonException {
15001530
}
15011531

15021532
/**
1503-
* Convenience overload for {@link #readValueCollecting(JsonParser)}.
1533+
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
15041534
*/
15051535
@SuppressWarnings("unchecked")
1506-
public <T> T readValueCollecting(File src) throws JacksonException {
1536+
public <T> T readValueCollectingProblems(File src) throws JacksonException {
15071537
_assertNotNull("src", src);
15081538
DeserializationContextExt ctxt = _deserializationContext();
15091539
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, src), true);
15101540
try {
1511-
return readValueCollecting(p);
1541+
return readValueCollectingProblems(p);
15121542
} finally {
15131543
try {
15141544
p.close();
@@ -1519,15 +1549,15 @@ public <T> T readValueCollecting(File src) throws JacksonException {
15191549
}
15201550

15211551
/**
1522-
* Convenience overload for {@link #readValueCollecting(JsonParser)}.
1552+
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
15231553
*/
15241554
@SuppressWarnings("unchecked")
1525-
public <T> T readValueCollecting(InputStream src) throws JacksonException {
1555+
public <T> T readValueCollectingProblems(InputStream src) throws JacksonException {
15261556
_assertNotNull("src", src);
15271557
DeserializationContextExt ctxt = _deserializationContext();
15281558
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, src), true);
15291559
try {
1530-
return readValueCollecting(p);
1560+
return readValueCollectingProblems(p);
15311561
} finally {
15321562
try {
15331563
p.close();
@@ -1538,15 +1568,15 @@ public <T> T readValueCollecting(InputStream src) throws JacksonException {
15381568
}
15391569

15401570
/**
1541-
* Convenience overload for {@link #readValueCollecting(JsonParser)}.
1571+
* Convenience overload for {@link #readValueCollectingProblems(JsonParser)}.
15421572
*/
15431573
@SuppressWarnings("unchecked")
1544-
public <T> T readValueCollecting(Reader src) throws JacksonException {
1574+
public <T> T readValueCollectingProblems(Reader src) throws JacksonException {
15451575
_assertNotNull("src", src);
15461576
DeserializationContextExt ctxt = _deserializationContext();
15471577
JsonParser p = _considerFilter(_parserFactory.createParser(ctxt, src), true);
15481578
try {
1549-
return readValueCollecting(p);
1579+
return readValueCollectingProblems(p);
15501580
} finally {
15511581
try {
15521582
p.close();

0 commit comments

Comments
 (0)