Skip to content

Commit 631515e

Browse files
Apply GPT-5 review fixes for issue #1196
Address review feedback from specs/issue-1196-collecting-deserialization-errors-claude-review-gpt-5.md: API Surface & Delegation: - Fix ObjectReader.readValueCollecting() to use public API `this.with(perCallAttrs).readValue(p)` instead of protected `_new(...)` factory - Maintains consistency with Jackson's builder pattern and public surface Limit Resolution: - Read max-problem cap from per-call reader config instead of base _config - Properly honors per-call attribute overrides - Affects both normal completion and hard failure paths Javadoc Enhancements: - Add comprehensive class-level Javadoc to DeferredBindingException with usage examples - Enhance CollectedProblem Javadoc explaining all fields, truncation, and immutability - Expand CollectingProblemHandler Javadoc detailing design, recoverable errors, and DoS protection - Improve ObjectReader.readValueCollecting() Javadoc noting behavior without collectErrors() and parser filtering differences Testing: - All 27 CollectingErrorsTest tests pass - Full suite: 4,662 tests pass, 0 failures, 0 errors - No regressions introduced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7ab4bd8 commit 631515e

File tree

4 files changed

+108
-19
lines changed

4 files changed

+108
-19
lines changed

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,16 +1386,30 @@ public <T> T readValue(TokenBuffer src) throws JacksonException
13861386
* errors if encountered. If any problems were collected, throws
13871387
* {@link tools.jackson.databind.exc.DeferredBindingException} with all problems.
13881388
*
1389-
* <p>On hard failures (non-recoverable errors), the original exception
1390-
* is thrown with collected problems attached as suppressed exceptions.
1389+
* <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
1392+
* {@link #readValue(JsonParser)} since no handler is registered.
1393+
*
1394+
* <p><b>Error handling</b>:
1395+
* <ul>
1396+
* <li>Recoverable errors are accumulated and thrown as
1397+
* {@link tools.jackson.databind.exc.DeferredBindingException} after parsing</li>
1398+
* <li>Hard (non-recoverable) failures throw immediately, with collected problems
1399+
* attached as suppressed exceptions</li>
1400+
* <li>When the configured limit is reached, collection stops</li>
1401+
* </ul>
13911402
*
13921403
* <p><b>Thread-safety</b>: Each call allocates a fresh problem bucket,
13931404
* so multiple concurrent calls on the same reader instance are safe.
13941405
*
1395-
* <p>This method should only be called on an ObjectReader created via
1396-
* {@link #collectErrors()}. If called on a regular reader, it behaves
1397-
* the same as {@link #readValue(JsonParser)}.
1406+
* <p><b>Parser filtering</b>: Unlike convenience overloads ({@link #readValueCollecting(String)},
1407+
* {@link #readValueCollecting(byte[])}, etc.), this method does <i>not</i> apply
1408+
* parser filtering. Callers are responsible for filter wrapping if needed.
13981409
*
1410+
* @param <T> Type to deserialize
1411+
* @param p JsonParser to read from (will not be closed by this method)
1412+
* @return Deserialized object
13991413
* @throws tools.jackson.databind.exc.DeferredBindingException if recoverable problems were collected
14001414
* @throws tools.jackson.databind.DatabindException if a non-recoverable error occurred
14011415
* @since 3.1
@@ -1410,21 +1424,17 @@ public <T> T readValueCollecting(JsonParser p) throws JacksonException {
14101424
ContextAttributes perCallAttrs = _config.getAttributes()
14111425
.withPerCallAttribute(tools.jackson.databind.deser.CollectingProblemHandler.class, bucket);
14121426

1413-
// Create a temporary ObjectReader with per-call attributes
1414-
// This matches the existing API surface (no new internal methods needed)
1415-
ObjectReader perCallReader = _new(this,
1416-
_config.with(perCallAttrs),
1417-
_valueType, _rootDeserializer, _valueToUpdate,
1418-
_schema, _injectableValues);
1427+
// Create a temporary ObjectReader with per-call attributes using public API
1428+
ObjectReader perCallReader = this.with(perCallAttrs);
14191429

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

14241434
// Check if any problems were collected
14251435
if (!bucket.isEmpty()) {
1426-
// Check if limit was reached
1427-
Integer maxProblems = (Integer) _config.getAttributes()
1436+
// Check if limit was reached - read from per-call config to honor overrides
1437+
Integer maxProblems = (Integer) perCallReader.getConfig().getAttributes()
14281438
.getAttribute(tools.jackson.databind.deser.CollectingProblemHandler.ATTR_MAX_PROBLEMS);
14291439
boolean limitReached = (maxProblems != null &&
14301440
bucket.size() >= maxProblems);
@@ -1440,7 +1450,8 @@ public <T> T readValueCollecting(JsonParser p) throws JacksonException {
14401450
} catch (DatabindException e) {
14411451
// Hard failure occurred; attach collected problems as suppressed
14421452
if (!bucket.isEmpty()) {
1443-
Integer maxProblems = (Integer) _config.getAttributes()
1453+
// Read from per-call config to honor overrides
1454+
Integer maxProblems = (Integer) perCallReader.getConfig().getAttributes()
14441455
.getAttribute(tools.jackson.databind.deser.CollectingProblemHandler.ATTR_MAX_PROBLEMS);
14451456
boolean limitReached = (maxProblems != null &&
14461457
bucket.size() >= maxProblems);

src/main/java/tools/jackson/databind/deser/CollectingProblemHandler.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,38 @@
1919
* problems into a per-call bucket stored in {@link DeserializationContext}
2020
* attributes.
2121
*
22-
* <p>Designed for use with {@link tools.jackson.databind.ObjectReader#collectErrors()}.
22+
* <p><b>Design</b>: This handler is completely stateless. The problem collection
23+
* bucket is allocated per-call by
24+
* {@link tools.jackson.databind.ObjectReader#readValueCollecting ObjectReader.readValueCollecting(...)}
25+
* and stored in per-call {@link tools.jackson.databind.cfg.ContextAttributes ContextAttributes},
26+
* ensuring thread-safety and call isolation.
27+
*
28+
* <p><b>Usage</b>: This class is internal infrastructure, registered automatically by
29+
* {@link tools.jackson.databind.ObjectReader#collectErrors() ObjectReader.collectErrors()}.
30+
* Users should not instantiate or register this handler manually.
31+
*
32+
* <p><b>Recoverable errors handled</b>:
33+
* <ul>
34+
* <li>Unknown properties ({@link #handleUnknownProperty handleUnknownProperty}) - skips children</li>
35+
* <li>Type coercion failures ({@link #handleWeirdStringValue handleWeirdStringValue},
36+
* {@link #handleWeirdNumberValue handleWeirdNumberValue}) - returns defaults</li>
37+
* <li>Map key coercion ({@link #handleWeirdKey handleWeirdKey}) - returns {@code NOT_HANDLED}</li>
38+
* <li>Instantiation failures ({@link #handleInstantiationProblem handleInstantiationProblem}) -
39+
* returns null when safe</li>
40+
* </ul>
41+
*
42+
* <p><b>Default values</b>: Primitives receive zero/false defaults; reference types
43+
* (including boxed primitives) receive {@code null} to avoid masking nullability issues.
44+
*
45+
* <p><b>DoS protection</b>: Collection stops when the configured limit (default 100)
46+
* is reached, preventing memory/CPU exhaustion attacks.
47+
*
48+
* <p><b>JSON Pointer</b>: Paths are built from parser context following RFC 6901,
49+
* with proper escaping of {@code ~} and {@code /} characters.
2350
*
2451
* @since 3.1
52+
* @see tools.jackson.databind.ObjectReader#collectErrors()
53+
* @see tools.jackson.databind.exc.DeferredBindingException
2554
*/
2655
public class CollectingProblemHandler extends DeserializationProblemHandler {
2756

src/main/java/tools/jackson/databind/exc/CollectedProblem.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,28 @@
1111
* Immutable value object capturing details about a single deserialization
1212
* problem encountered during error-collecting mode.
1313
*
14+
* <p><b>Contents</b>: Each problem records:
15+
* <ul>
16+
* <li>{@link #getPath() path} - RFC 6901 JSON Pointer to the problematic field
17+
* (e.g., {@code "/items/2/price"})</li>
18+
* <li>{@link #getMessage() message} - Human-readable error description</li>
19+
* <li>{@link #getTargetType() targetType} - Expected Java type (may be null)</li>
20+
* <li>{@link #getLocation() location} - Source location in JSON (line/column)</li>
21+
* <li>{@link #getRawValue() rawValue} - Original value from JSON that caused the error
22+
* (truncated if > 200 chars)</li>
23+
* <li>{@link #getToken() token} - JSON token type at error location</li>
24+
* </ul>
25+
*
26+
* <p><b>Truncation</b>: String values longer than {@value #MAX_RAW_VALUE_LENGTH}
27+
* characters are truncated with "..." suffix to prevent memory issues.
28+
*
29+
* <p><b>Unknown properties</b>: For unknown property errors, {@code rawValue}
30+
* is {@code null} since the property name is already in the path.
31+
*
32+
* <p><b>Immutability</b>: All instances are immutable and thread-safe.
33+
*
1434
* @since 3.1
35+
* @see DeferredBindingException#getProblems()
1536
*/
1637
public final class CollectedProblem {
1738
/**

src/main/java/tools/jackson/databind/exc/DeferredBindingException.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,39 @@
88

99
/**
1010
* Exception that aggregates multiple recoverable deserialization problems
11-
* encountered during error-collecting mode (enabled via
12-
* {@link tools.jackson.databind.ObjectReader#collectErrors()}).
11+
* encountered during error-collecting mode.
1312
*
14-
* <p>Each problem is captured as a {@link CollectedProblem} containing
15-
* the error location, message, and context.
13+
* <p><b>Usage</b>: This exception is thrown by
14+
* {@link tools.jackson.databind.ObjectReader#readValueCollecting ObjectReader.readValueCollecting(...)}
15+
* when one or more recoverable errors were collected during deserialization.
16+
* Enable error collection via {@link tools.jackson.databind.ObjectReader#collectErrors()}.
17+
*
18+
* <p><b>Problem access</b>: Each problem is captured as a {@link CollectedProblem}
19+
* containing the JSON Pointer path, error message, location, target type, raw value, and token.
20+
* Access problems via {@link #getProblems()}.
21+
*
22+
* <p><b>Limit handling</b>: When the configured problem limit is reached, collection
23+
* stops and {@link #isLimitReached()} returns {@code true}. This indicates additional
24+
* errors may exist beyond those collected.
25+
*
26+
* <p><b>Message formatting</b>: The exception message shows:
27+
* <ul>
28+
* <li>For 1 problem: the single error message</li>
29+
* <li>For multiple: count + first 5 problems + "...and N more" suffix</li>
30+
* <li>A "limit reached" note if applicable</li>
31+
* </ul>
32+
*
33+
* <p><b>Example</b>:
34+
* <pre>{@code
35+
* try {
36+
* MyBean bean = reader.collectErrors()
37+
* .readValueCollecting(json);
38+
* } catch (DeferredBindingException e) {
39+
* for (CollectedProblem p : e.getProblems()) {
40+
* System.err.println("Error at " + p.getPath() + ": " + p.getMessage());
41+
* }
42+
* }
43+
* }</pre>
1644
*
1745
* @since 3.1
1846
*/

0 commit comments

Comments
 (0)