-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Implement error collection for deserialization (#1196) #5364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 3.x
Are you sure you want to change the base?
Changes from all commits
4f94e17
290d944
875c953
6914713
b6ba90b
24604ba
f0c4342
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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). | ||
| * | ||
| * <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. | ||
| * 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the throws should be included in the javadoc There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
| } | ||
|
|
@@ -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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the idea to only catch There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
problemCollectingReadermethod.