@@ -692,6 +692,61 @@ public ObjectReader withHandler(DeserializationProblemHandler h) {
692692 return _with (_config .withHandler (h ));
693693 }
694694
695+ /**
696+ * Enables error collection mode by registering a
697+ * {@link tools.jackson.databind.deser.CollectingProblemHandler} with default
698+ * error limit (100 problems).
699+ *
700+ * <p>The returned reader is immutable and thread-safe. Each call to
701+ * {@link #readValueCollecting} allocates a fresh problem bucket, so concurrent
702+ * calls do not interfere.
703+ *
704+ * <p>Usage:
705+ * <pre>
706+ * ObjectReader reader = mapper.reader()
707+ * .forType(MyBean.class)
708+ * .collectErrors();
709+ *
710+ * MyBean bean = reader.readValueCollecting(json);
711+ * </pre>
712+ *
713+ * @return A new ObjectReader configured for error collection
714+ * @since 3.1
715+ */
716+ public ObjectReader collectErrors () {
717+ return collectErrors (100 ); // Default limit
718+ }
719+
720+ /**
721+ * Enables error collection mode with a custom problem limit.
722+ *
723+ * <p><b>Thread-safety</b>: The returned reader is immutable and thread-safe.
724+ * Each call to {@link #readValueCollecting} allocates a fresh problem bucket,
725+ * so concurrent calls do not interfere.
726+ *
727+ * @param maxProblems Maximum number of problems to collect (must be > 0)
728+ * @return A new ObjectReader configured for error collection
729+ * @since 3.1
730+ */
731+ public ObjectReader collectErrors (int maxProblems ) {
732+ if (maxProblems <= 0 ) {
733+ throw new IllegalArgumentException ("maxProblems must be positive" );
734+ }
735+
736+ // Store ONLY the max limit in config (not the bucket)
737+ // Bucket will be allocated fresh per-call in readValueCollecting()
738+ ContextAttributes attrs = _config .getAttributes ()
739+ .withSharedAttribute (tools .jackson .databind .deser .CollectingProblemHandler .ATTR_MAX_PROBLEMS , maxProblems );
740+
741+ DeserializationConfig newConfig = _config
742+ .withHandler (new tools .jackson .databind .deser .CollectingProblemHandler ())
743+ .with (attrs );
744+
745+ // Return new immutable reader (no mutable state)
746+ return _new (this , newConfig , _valueType , _rootDeserializer , _valueToUpdate ,
747+ _schema , _injectableValues );
748+ }
749+
695750 public ObjectReader with (Base64Variant defaultBase64 ) {
696751 return _with (_config .with (defaultBase64 ));
697752 }
@@ -1320,6 +1375,176 @@ public <T> T readValue(TokenBuffer src) throws JacksonException
13201375 _considerFilter (src .asParser (ctxt ) , false ));
13211376 }
13221377
1378+ /*
1379+ /**********************************************************************
1380+ /* Deserialization methods with error collection
1381+ /**********************************************************************
1382+ */
1383+
1384+ /**
1385+ * 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.
1388+ *
1389+ * <p>On hard failures (non-recoverable errors), the original exception
1390+ * is thrown with collected problems attached as suppressed exceptions.
1391+ *
1392+ * <p><b>Thread-safety</b>: Each call allocates a fresh problem bucket,
1393+ * so multiple concurrent calls on the same reader instance are safe.
1394+ *
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)}.
1398+ *
1399+ * @throws tools.jackson.databind.exc.DeferredBindingException if recoverable problems were collected
1400+ * @throws tools.jackson.databind.DatabindException if a non-recoverable error occurred
1401+ * @since 3.1
1402+ */
1403+ public <T > T readValueCollecting (JsonParser p ) throws JacksonException {
1404+ _assertNotNull ("p" , p );
1405+
1406+ // CRITICAL: Allocate a FRESH bucket for THIS call (thread-safety)
1407+ List <tools .jackson .databind .exc .CollectedProblem > bucket = new ArrayList <>();
1408+
1409+ // Create per-call attributes with the fresh bucket
1410+ ContextAttributes perCallAttrs = _config .getAttributes ()
1411+ .withPerCallAttribute (tools .jackson .databind .deser .CollectingProblemHandler .class , bucket );
1412+
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 );
1419+
1420+ try {
1421+ // Delegate to the temporary reader's existing readValue method
1422+ T result = perCallReader .readValue (p );
1423+
1424+ // Check if any problems were collected
1425+ if (!bucket .isEmpty ()) {
1426+ // Check if limit was reached
1427+ Integer maxProblems = (Integer ) _config .getAttributes ()
1428+ .getAttribute (tools .jackson .databind .deser .CollectingProblemHandler .ATTR_MAX_PROBLEMS );
1429+ boolean limitReached = (maxProblems != null &&
1430+ bucket .size () >= maxProblems );
1431+
1432+ throw new tools .jackson .databind .exc .DeferredBindingException (p , bucket , limitReached );
1433+ }
1434+
1435+ return result ;
1436+
1437+ } catch (tools .jackson .databind .exc .DeferredBindingException e ) {
1438+ throw e ; // Already properly formatted
1439+
1440+ } catch (DatabindException e ) {
1441+ // Hard failure occurred; attach collected problems as suppressed
1442+ if (!bucket .isEmpty ()) {
1443+ Integer maxProblems = (Integer ) _config .getAttributes ()
1444+ .getAttribute (tools .jackson .databind .deser .CollectingProblemHandler .ATTR_MAX_PROBLEMS );
1445+ boolean limitReached = (maxProblems != null &&
1446+ bucket .size () >= maxProblems );
1447+
1448+ e .addSuppressed (new tools .jackson .databind .exc .DeferredBindingException (p , bucket , limitReached ));
1449+ }
1450+ throw e ;
1451+ }
1452+ }
1453+
1454+ /**
1455+ * Convenience overload for {@link #readValueCollecting(JsonParser)}.
1456+ */
1457+ public <T > T readValueCollecting (String content ) throws JacksonException {
1458+ _assertNotNull ("content" , content );
1459+ DeserializationContextExt ctxt = _deserializationContext ();
1460+ JsonParser p = _considerFilter (_parserFactory .createParser (ctxt , content ), true );
1461+ try {
1462+ return readValueCollecting (p );
1463+ } finally {
1464+ try {
1465+ p .close ();
1466+ } catch (Exception e ) {
1467+ // ignore
1468+ }
1469+ }
1470+ }
1471+
1472+ /**
1473+ * Convenience overload for {@link #readValueCollecting(JsonParser)}.
1474+ */
1475+ @ SuppressWarnings ("unchecked" )
1476+ public <T > T readValueCollecting (byte [] content ) throws JacksonException {
1477+ _assertNotNull ("content" , content );
1478+ DeserializationContextExt ctxt = _deserializationContext ();
1479+ JsonParser p = _considerFilter (_parserFactory .createParser (ctxt , content ), true );
1480+ try {
1481+ return readValueCollecting (p );
1482+ } finally {
1483+ try {
1484+ p .close ();
1485+ } catch (Exception e ) {
1486+ // ignore
1487+ }
1488+ }
1489+ }
1490+
1491+ /**
1492+ * Convenience overload for {@link #readValueCollecting(JsonParser)}.
1493+ */
1494+ @ SuppressWarnings ("unchecked" )
1495+ public <T > T readValueCollecting (File src ) throws JacksonException {
1496+ _assertNotNull ("src" , src );
1497+ DeserializationContextExt ctxt = _deserializationContext ();
1498+ JsonParser p = _considerFilter (_parserFactory .createParser (ctxt , src ), true );
1499+ try {
1500+ return readValueCollecting (p );
1501+ } finally {
1502+ try {
1503+ p .close ();
1504+ } catch (Exception e ) {
1505+ // ignore
1506+ }
1507+ }
1508+ }
1509+
1510+ /**
1511+ * Convenience overload for {@link #readValueCollecting(JsonParser)}.
1512+ */
1513+ @ SuppressWarnings ("unchecked" )
1514+ public <T > T readValueCollecting (InputStream src ) throws JacksonException {
1515+ _assertNotNull ("src" , src );
1516+ DeserializationContextExt ctxt = _deserializationContext ();
1517+ JsonParser p = _considerFilter (_parserFactory .createParser (ctxt , src ), true );
1518+ try {
1519+ return readValueCollecting (p );
1520+ } finally {
1521+ try {
1522+ p .close ();
1523+ } catch (Exception e ) {
1524+ // ignore
1525+ }
1526+ }
1527+ }
1528+
1529+ /**
1530+ * Convenience overload for {@link #readValueCollecting(JsonParser)}.
1531+ */
1532+ @ SuppressWarnings ("unchecked" )
1533+ public <T > T readValueCollecting (Reader src ) throws JacksonException {
1534+ _assertNotNull ("src" , src );
1535+ DeserializationContextExt ctxt = _deserializationContext ();
1536+ JsonParser p = _considerFilter (_parserFactory .createParser (ctxt , src ), true );
1537+ try {
1538+ return readValueCollecting (p );
1539+ } finally {
1540+ try {
1541+ p .close ();
1542+ } catch (Exception e ) {
1543+ // ignore
1544+ }
1545+ }
1546+ }
1547+
13231548 /*
13241549 /**********************************************************************
13251550 /* Deserialization methods; JsonNode ("tree")
0 commit comments