This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 5fc19f5dd4b0 CAMEL-17598: camel-bindy: continue unmarshal on per-field
parse failure (#23922)
5fc19f5dd4b0 is described below
commit 5fc19f5dd4b0a9070e53ce83a823673d54d80bea
Author: Vishal Nagaraj <[email protected]>
AuthorDate: Sat Jun 13 12:05:21 2026 +0530
CAMEL-17598: camel-bindy: continue unmarshal on per-field parse failure
(#23922)
Today a single malformed value (a bad date, a non-numeric int, etc.)
aborts the entire Bindy unmarshal even when the rest of the row would
have parsed fine. Add an opt-in tolerance mode that catches per-field
parse failures and substitutes a fallback so processing continues.
Two new annotation elements drive it: continueParseOnFailure on
@CsvRecord, @FixedLengthRecord, @Message (record-level boolean, default
false) and continueParseOnFailure on @DataField, @KeyValuePairField
(field-level tri-state ResumeUnmarshalingState, default INHERIT). The
field value can override the record default; INHERIT defers to the
record. All defaults preserve today's strict behaviour.
When tolerant and parsing fails, Bindy substitutes @DataField.defaultValue
parsed through the same Format, so the substitute is automatically the
right type. With no defaultValue, the field gets the existing
getDefaultValueForPrimitive convention: null for objects, "" for String,
false for boolean, MIN_VALUE for numeric primitives.
@KeyValuePairField has no defaultValue today, so KVP fields only get
the primitive/null fallback.
A pluggable ParserErrorHandler interface was discussed and intentionally
deferred to keep this PR small; can be added later as a non-breaking
addition if needed.
Tests cover the full override matrix and value-substitution paths
across all three factories. Full camel-bindy suite passes.
---
.../src/main/docs/bindy-dataformat.adoc | 100 ++++++
.../dataformat/bindy/BindyAbstractFactory.java | 35 ++
.../camel/dataformat/bindy/BindyCsvFactory.java | 13 +-
.../dataformat/bindy/BindyFixedLengthFactory.java | 7 +-
.../dataformat/bindy/BindyKeyValuePairFactory.java | 22 +-
.../bindy/annotation/ContinueOnFailure.java | 24 ++
.../dataformat/bindy/annotation/CsvRecord.java | 7 +
.../dataformat/bindy/annotation/DataField.java | 10 +
.../bindy/annotation/FixedLengthRecord.java | 7 +
.../bindy/annotation/KeyValuePairField.java | 10 +
.../camel/dataformat/bindy/annotation/Message.java | 7 +
.../csv2/BindyCsvContinueOnParseFailureTest.java | 376 +++++++++++++++++++++
.../fix/BindyKvpContinueOnParseFailureTest.java | 237 +++++++++++++
.../BindyFixedContinueOnParseFailureTest.java | 303 +++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 36 ++
15 files changed, 1186 insertions(+), 8 deletions(-)
diff --git a/components/camel-bindy/src/main/docs/bindy-dataformat.adoc
b/components/camel-bindy/src/main/docs/bindy-dataformat.adoc
index 080c071b9739..95c9a813401c 100644
--- a/components/camel-bindy/src/main/docs/bindy-dataformat.adoc
+++ b/components/camel-bindy/src/main/docs/bindy-dataformat.adoc
@@ -112,6 +112,10 @@ expressions, e.g., the '\|' sign, then you have to mask
it, like '\|'
| autospanLine | boolean | | false a| Last record spans rest of line
(optional) - if enabled then the last column is auto spanned to end of line, for
example if it is a comment, etc this allows the line to contain all
characters, also the delimiter char.
+| continueParseOnFailure | boolean | | false a| If true, a parse failure on
any field in this record is replaced with the field's defaultValue (or the
+type-appropriate default if no defaultValue is set), instead of aborting the
unmarshal. Individual fields can opt
+out per-field via @DataField.continueParseOnFailure. Default false preserves
the existing fail-fast behavior.
+
| crlf | String | | WINDOWS a| Character to be used to add a carriage return
after each record (optional) - allow defining the carriage return
character to use. If you specify a value other than the three listed before,
the value you enter (custom) will be
used as the CRLF character(s). Three values can be used : WINDOWS, UNIX, MAC,
or custom.
@@ -430,6 +434,13 @@ int, date, ...) and optionally of a pattern.
| columnName | String | | a| Name of the header column (optional). Uses the
name of the property as default. Only applicable when `CsvRecord`
has `generateHeaderColumns = true`
+| continueParseOnFailure | ContinueOnFailure | | INHERIT a| Whether to keep
going when parsing this field fails.
+
+TRUE forces tolerance for this field — a parse error is replaced with the
field's defaultValue, or the
+type-appropriate default if no defaultValue is set. FALSE forces strict
behavior: the exception propagates and
+aborts the unmarshal as it always has. INHERIT (the default) defers to the
record-level setting on @CsvRecord
+or @FixedLengthRecord.
+
| decimalSeparator | String | | | Decimal Separator to be used with
BigDecimal number
| defaultValue | String | | | Field's default value in case no value is set
@@ -759,6 +770,10 @@ field, we can then add 'pad' characters.
|===
| Parameter name | Type | Required | Default value | Info
+| continueParseOnFailure | boolean | | false a| If true, a parse failure on
any field in this record is replaced with the field's defaultValue (or the
+type-appropriate default if no defaultValue is set), instead of aborting the
unmarshal. Individual fields can opt
+out per-field via @DataField.continueParseOnFailure. Default false preserves
the existing fail-fast behavior.
+
| countGrapheme | boolean | | false | Indicates how chars are counted
| crlf | String | | WINDOWS a| Character to be used to add a carriage return
after each record (optional). Possible values: WINDOWS, UNIX, MAC,
@@ -1213,6 +1228,10 @@ or 'anything'.
| pairSeparator | String | ✓ | | Pair separator used to split the key
value pairs in tokens (mandatory). Can be '=', ';', or 'anything'.
+| continueParseOnFailure | boolean | | false a| If true, a parse failure on
any field in this message is replaced with the type-appropriate default (null,
"",
+false, or MIN_VALUE for numeric primitives), instead of aborting the
unmarshal. Individual fields can opt out
+per-field via @KeyValuePairField.continueParseOnFailure. Default false
preserves the existing fail-fast behavior.
+
| crlf | String | | WINDOWS a| Character to be used to add a carriage return
after each record (optional). Possible values = WINDOWS, UNIX, MAC,
or custom. If you specify a value other than the three listed before, the
value you enter (custom) will be used
as the CRLF character(s).
@@ -1282,6 +1301,13 @@ pattern and if the field is required.
| tag | int | ✓ | | tag identifying the field in the message
(mandatory) - must be unique
+| continueParseOnFailure | ContinueOnFailure | | INHERIT a| Whether to keep
going when parsing this field fails.
+
+TRUE forces tolerance — a parse error is replaced with the type-appropriate
default (null, "", false, or
+MIN_VALUE for numeric primitives). KeyValuePairField doesn't currently have a
defaultValue element, so there's no
+user-supplied substitute available here. FALSE forces strict behavior. INHERIT
(the default) defers
+to @Message.continueParseOnFailure.
+
| impliedDecimalSeparator | boolean | | false | <b>Camel 2.11:</b> Indicates
if there is a decimal point implied at a specified location
| name | String | | | name of the field (optional)
@@ -1668,6 +1694,80 @@ public static class OrderNumberFormatFactory extends
AbstractFormatFactory {
}
----
+=== Handling parse failures
+
+By default, when Bindy can't parse a field — say a malformed date, or `"abc"`
in an `int` column — it
+throws and the whole unmarshal aborts, even if the rest of the row would have
parsed fine. You can
+opt into a tolerant mode where the bad field is replaced with a fallback value
and the row is delivered
+intact.
+
+==== Turning it on
+
+There are two annotation elements, on different levels:
+
+* `continueParseOnFailure` on the *record* annotation (`@CsvRecord`,
`@FixedLengthRecord`, `@Message`)
+ — a plain boolean that sets the default for every field in that record.
+* `continueParseOnFailure` on the *field* annotation (`@DataField`,
`@KeyValuePairField`) — a tri-state
+ enum (`ContinueOnFailure`) that can override the record default for one
specific field.
+
+The tri-state lets a single strict field live inside an otherwise-tolerant
record, or vice versa:
+
+[width="100%",cols="34%,33%,33%",options="header"]
+|===
+| `@CsvRecord` | `@DataField` | Effective behavior
+| (unset, `false`) | `INHERIT` (default) | Strict — exception propagates, like
today
+| `true` | `INHERIT` | Tolerant
+| `true` | `FALSE` | Strict (the field overrides the record)
+| `false` | `TRUE` | Tolerant (the field overrides the record)
+|===
+
+==== What the tolerant path substitutes
+
+When a parse fails and the field is tolerant, Bindy picks a substitute value
in this order:
+
+. If the field has a `@DataField.defaultValue` set, it's parsed through the
*same* `Format` that just
+ failed. So a `defaultValue = "1970-01-01"` on a `Date` field becomes a real
`Date`, a
+ `defaultValue = "0.00"` on a `BigDecimal` becomes a real `BigDecimal`, and
so on. The type is
+ always correct.
+. Otherwise the field gets the same fallback an unfilled field would get:
`null` for object types
+ (`Date`, `BigDecimal`, `String` with `defaultValueStringAsNull=true`, custom
converters), `""` for
+ `String`, `false` for `boolean`, and `MIN_VALUE` for numeric primitives
(`int`, `long`, `byte`,
+ `short`, `float`, `double`, `char`).
+
+[NOTE]
+====
+The `MIN_VALUE` sentinel for numeric primitives is Bindy's existing convention
from
+`getDefaultValueForPrimitive`; it's not specific to this feature. If you want
`0` instead, set an
+explicit `defaultValue = "0"` on the field.
+====
+
+==== Example
+
+[source,java]
+----
+@CsvRecord(separator = ",", continueParseOnFailure = true)
+public class Order {
+ @DataField(pos = 1)
+ private int id; // bad int -> Integer.MIN_VALUE
+
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", defaultValue = "1970-01-01")
+ private Date orderDate; // bad date -> 1970-01-01
+
+ @DataField(pos = 3, continueParseOnFailure = ContinueOnFailure.FALSE)
+ private BigDecimal amount; // bad number still throws; this
field is exempt
+}
+----
+
+A row like `1,2026-01-15,42.50` parses normally. A row like
`xyz,not-a-date,42.50` produces an `Order`
+with `id = Integer.MIN_VALUE`, `orderDate = 1970-01-01`, and `amount = 42.50`.
A row with a bad
+`amount` (e.g. `1,2026-01-15,xyz`) still throws, because that field is
annotated strict.
+
+==== KVP caveat
+
+`@KeyValuePairField` does not currently have a `defaultValue` element, so KVP
fields can only fall
+back to the type-appropriate default (`null`, `""`, `false`, or `MIN_VALUE`).
Everything else works
+the same as CSV and fixed-length.
+
=== Supported Datatypes
The DefaultFormatFactory makes formatting of the following datatype available
by
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyAbstractFactory.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyAbstractFactory.java
index add9f55cffe8..eea74829d8f5 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyAbstractFactory.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyAbstractFactory.java
@@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Set;
import org.apache.camel.CamelContext;
+import org.apache.camel.dataformat.bindy.annotation.ContinueOnFailure;
import org.apache.camel.dataformat.bindy.annotation.Link;
import org.apache.camel.dataformat.bindy.annotation.OneToMany;
import org.apache.camel.support.ObjectHelper;
@@ -50,6 +51,7 @@ public abstract class BindyAbstractFactory implements
BindyFactory {
private String locale;
private Class<?> type;
private boolean defaultValueStringAsNull;
+ protected boolean continueParseOnFailure;
protected BindyAbstractFactory(Class<?> type) throws Exception {
this.type = type;
@@ -204,6 +206,39 @@ public abstract class BindyAbstractFactory implements
BindyFactory {
return Integer.valueOf(keyGenerated);
}
+ protected boolean shouldContinueOnFailure(ContinueOnFailure fieldOpinion) {
+ return switch (fieldOpinion) {
+ case TRUE -> true;
+ case FALSE -> false;
+ case INHERIT -> continueParseOnFailure;
+ };
+ }
+
+ /**
+ * Parse a single field value with optional tolerance for failures.
+ *
+ * If parsing succeeds, returns the parsed value. If it fails and {@code
continueOnFailure} is false, the original
+ * exception is rethrown so the caller can wrap it with position/line
context (this is the existing fail-fast
+ * behavior). If it fails and {@code continueOnFailure} is true, returns
the field's {@code defaultValue} parsed
+ * through the same Format, or — if no defaultValue is set — the
type-appropriate default from
+ * {@link #getDefaultValueForPrimitive}.
+ */
+ protected Object parseField(
+ Format format, String string, boolean continueOnFailure, Class<?>
fieldType, String defaultValue)
+ throws Exception {
+ try {
+ return format.parse(string);
+ } catch (Exception e) {
+ if (!continueOnFailure) {
+ throw e;
+ }
+ if (!defaultValue.isEmpty()) {
+ return format.parse(defaultValue);
+ }
+ return getDefaultValueForPrimitive(fieldType,
isDefaultValueStringAsNull());
+ }
+ }
+
private static NumberFormat getNumberFormat() {
// Get instance of NumberFormat
NumberFormat nf = NumberFormat.getInstance();
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
index 9c095e08402a..b9fcf9cbbf9a 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
@@ -215,6 +215,8 @@ public class BindyCsvFactory extends BindyAbstractFactory
implements BindyFactor
data = data.trim();
}
+ boolean fieldContinueParseOnFailure =
shouldContinueOnFailure(dataField.continueParseOnFailure());
+
if (dataField.required()) {
// Increment counter of mandatory fields
++counterMandatoryFields;
@@ -251,14 +253,16 @@ public class BindyCsvFactory extends BindyAbstractFactory
implements BindyFactor
if (!data.isEmpty()) {
try {
if (quoting && quote != null && (data.contains("\\" + quote)
|| data.contains(quote)) && quotingEscaped) {
- value = format.parse(data.replaceAll("\\\\" + quote, "\\"
+ quote));
+ value = parseField(format, data.replaceAll("\\\\" + quote,
"\\" + quote), fieldContinueParseOnFailure,
+ field.getType(), dataField.defaultValue());
} else if (quote != null && quote.equals(DOUBLE_QUOTES_SYMBOL)
&& data.contains(DOUBLE_QUOTES_SYMBOL +
DOUBLE_QUOTES_SYMBOL) && !quotingEscaped) {
// If double-quotes are used to enclose fields, the two
double
// quotes character must be replaced with one according to
RFC 4180 section 2.7
- value = format.parse(data.replace(DOUBLE_QUOTES_SYMBOL +
DOUBLE_QUOTES_SYMBOL, DOUBLE_QUOTES_SYMBOL));
+ value = parseField(format,
data.replace(DOUBLE_QUOTES_SYMBOL + DOUBLE_QUOTES_SYMBOL, DOUBLE_QUOTES_SYMBOL),
+ fieldContinueParseOnFailure, field.getType(),
dataField.defaultValue());
} else {
- value = format.parse(data);
+ value = parseField(format, data,
fieldContinueParseOnFailure, field.getType(), dataField.defaultValue());
}
} catch (FormatException ie) {
throw new IllegalArgumentException(ie.getMessage() + ",
position: " + pos + ", line: " + line, ie);
@@ -693,6 +697,9 @@ public class BindyCsvFactory extends BindyAbstractFactory
implements BindyFactor
trimLine = csvRecord.trimLine();
LOG.debug("Trim line: {}", trimLine);
+
+ continueParseOnFailure =
csvRecord.continueParseOnFailure();
+ LOG.debug("Continue parse on failure: {}",
continueParseOnFailure);
}
if (section != null) {
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyFixedLengthFactory.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyFixedLengthFactory.java
index 3a1934661d8c..ac772c6c34df 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyFixedLengthFactory.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyFixedLengthFactory.java
@@ -238,6 +238,8 @@ public class BindyFixedLengthFactory extends
BindyAbstractFactory implements Bin
//token = token.trim();
}
+ boolean fieldContinueParseOnFailure =
shouldContinueOnFailure(dataField.continueParseOnFailure());
+
// Check mandatory field
if (dataField.required()) {
@@ -279,7 +281,7 @@ public class BindyFixedLengthFactory extends
BindyAbstractFactory implements Bin
}
if (!token.isEmpty()) {
try {
- value = format.parse(token);
+ value = parseField(format, token,
fieldContinueParseOnFailure, field.getType(), dataField.defaultValue());
} catch (FormatException ie) {
throw new IllegalArgumentException(ie.getMessage() + ",
position: " + offset + ", line: " + line, ie);
} catch (Exception e) {
@@ -623,6 +625,9 @@ public class BindyFixedLengthFactory extends
BindyAbstractFactory implements Bin
countGrapheme = fixedLengthRecord.countGrapheme();
LOG.debug("Enable grapheme counting instead of codepoints:
{}", countGrapheme);
+
+ continueParseOnFailure =
fixedLengthRecord.continueParseOnFailure();
+ LOG.debug("Continue parse on failure: {}",
continueParseOnFailure);
}
}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyKeyValuePairFactory.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyKeyValuePairFactory.java
index 70d38f0eec27..84acd3dc28ea 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyKeyValuePairFactory.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyKeyValuePairFactory.java
@@ -293,8 +293,12 @@ public class BindyKeyValuePairFactory extends
BindyAbstractFactory implements Bi
getLocale());
Format<?> format =
formatFactory.getFormat(formattingOptions);
+ boolean fieldContinueParseOnFailure
+ =
shouldContinueOnFailure(keyValuePairField.continueParseOnFailure());
+
// format the value of the key received
- result = formatField(format, value, key, line);
+ result = formatField(format, value, key, line,
fieldContinueParseOnFailure, field.getType(),
+ "");
LOG.debug("Value formated : {}", result);
@@ -335,8 +339,12 @@ public class BindyKeyValuePairFactory extends
BindyAbstractFactory implements Bi
getLocale());
Format<?> format =
formatFactory.getFormat(formattingOptions);
+ boolean fieldContinueParseOnFailure
+ =
shouldContinueOnFailure(keyValuePairField.continueParseOnFailure());
+
// format the value of the key received
- Object result = formatField(format, value,
key, line);
+ Object result = formatField(format, value,
key, line, fieldContinueParseOnFailure,
+ field.getType(), "");
LOG.debug("Value formated : {}", result);
@@ -573,7 +581,10 @@ public class BindyKeyValuePairFactory extends
BindyAbstractFactory implements Bi
return builder.toString();
}
- private Object formatField(Format<?> format, String value, int tag, int
line) throws Exception {
+ private Object formatField(
+ Format<?> format, String value, int tag, int line, boolean
continueOnFailure, Class<?> fieldType,
+ String defaultValue)
+ throws Exception {
Object obj = null;
@@ -581,7 +592,7 @@ public class BindyKeyValuePairFactory extends
BindyAbstractFactory implements Bi
// Format field value
try {
- obj = format.parse(value);
+ obj = parseField(format, value, continueOnFailure, fieldType,
defaultValue);
} catch (Exception e) {
throw new IllegalArgumentException(
"Parsing error detected for field defined at the tag:
" + tag + ", line: " + line, e);
@@ -648,6 +659,9 @@ public class BindyKeyValuePairFactory extends
BindyAbstractFactory implements Bi
// Get isOrdered parameter
messageOrdered = message.isOrdered();
LOG.debug("Is the message ordered in output: {}",
messageOrdered);
+
+ continueParseOnFailure = message.continueParseOnFailure();
+ LOG.debug("Continue parse on failure: {}",
continueParseOnFailure);
}
if (section != null) {
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/ContinueOnFailure.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/ContinueOnFailure.java
new file mode 100644
index 000000000000..2c92213f9c18
--- /dev/null
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/ContinueOnFailure.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dataformat.bindy.annotation;
+
+public enum ContinueOnFailure {
+
+ FALSE,
+ TRUE,
+ INHERIT
+}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/CsvRecord.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/CsvRecord.java
index 5e1d461b5c5d..e2012370a7d2 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/CsvRecord.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/CsvRecord.java
@@ -124,4 +124,11 @@ public @interface CsvRecord {
*/
boolean trimLine() default true;
+ /**
+ * If true, a parse failure on any field in this record is replaced with
the field's defaultValue (or the
+ * type-appropriate default if no defaultValue is set), instead of
aborting the unmarshal. Individual fields can opt
+ * out per-field via @DataField.continueParseOnFailure. Default false
preserves the existing fail-fast behavior.
+ */
+ boolean continueParseOnFailure() default false;
+
}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/DataField.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/DataField.java
index 1b79525574a3..4ef891393cd3 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/DataField.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/DataField.java
@@ -152,4 +152,14 @@ public @interface DataField {
*
org.apache.camel.dataformat.bindy.csv.BindySimpleCsvFunctionWithExternalMethodTest.replaceToBar
*/
String method() default "";
+
+ /**
+ * Whether to keep going when parsing this field fails.
+ *
+ * TRUE forces tolerance for this field — a parse error is replaced with
the field's defaultValue, or the
+ * type-appropriate default if no defaultValue is set. FALSE forces strict
behavior: the exception propagates and
+ * aborts the unmarshal as it always has. INHERIT (the default) defers to
the record-level setting on @CsvRecord
+ * or @FixedLengthRecord.
+ */
+ ContinueOnFailure continueParseOnFailure() default
ContinueOnFailure.INHERIT;
}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/FixedLengthRecord.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/FixedLengthRecord.java
index b54e3317b302..94bd5af9ef23 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/FixedLengthRecord.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/FixedLengthRecord.java
@@ -107,4 +107,11 @@ public @interface FixedLengthRecord {
* Indicates how chars are counted
*/
boolean countGrapheme() default false;
+
+ /**
+ * If true, a parse failure on any field in this record is replaced with
the field's defaultValue (or the
+ * type-appropriate default if no defaultValue is set), instead of
aborting the unmarshal. Individual fields can opt
+ * out per-field via @DataField.continueParseOnFailure. Default false
preserves the existing fail-fast behavior.
+ */
+ boolean continueParseOnFailure() default false;
}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/KeyValuePairField.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/KeyValuePairField.java
index 88863bde0b62..87f2fc2cab26 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/KeyValuePairField.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/KeyValuePairField.java
@@ -85,4 +85,14 @@ public @interface KeyValuePairField {
* <b>Camel 2.11:</b> Indicates if there is a decimal point implied at a
specified location
*/
boolean impliedDecimalSeparator() default false;
+
+ /**
+ * Whether to keep going when parsing this field fails.
+ *
+ * TRUE forces tolerance — a parse error is replaced with the
type-appropriate default (null, "", false, or
+ * MIN_VALUE for numeric primitives). KeyValuePairField doesn't currently
have a defaultValue element, so there's no
+ * user-supplied substitute available here. FALSE forces strict behavior.
INHERIT (the default) defers
+ * to @Message.continueParseOnFailure.
+ */
+ ContinueOnFailure continueParseOnFailure() default
ContinueOnFailure.INHERIT;
}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/Message.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/Message.java
index d2aab5eb50fe..a318674c268d 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/Message.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/annotation/Message.java
@@ -82,4 +82,11 @@ public @interface Message {
* @return boolean
*/
boolean isOrdered() default false;
+
+ /**
+ * If true, a parse failure on any field in this message is replaced with
the type-appropriate default (null, "",
+ * false, or MIN_VALUE for numeric primitives), instead of aborting the
unmarshal. Individual fields can opt out
+ * per-field via @KeyValuePairField.continueParseOnFailure. Default false
preserves the existing fail-fast behavior.
+ */
+ boolean continueParseOnFailure() default false;
}
diff --git
a/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/csv2/BindyCsvContinueOnParseFailureTest.java
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/csv2/BindyCsvContinueOnParseFailureTest.java
new file mode 100644
index 000000000000..54f5b4d3f6e2
--- /dev/null
+++
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/csv2/BindyCsvContinueOnParseFailureTest.java
@@ -0,0 +1,376 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dataformat.bindy.csv2;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.dataformat.bindy.annotation.ContinueOnFailure;
+import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
+import org.apache.camel.dataformat.bindy.annotation.DataField;
+import org.apache.camel.dataformat.bindy.csv.BindyCsvDataFormat;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Verifies the continueParseOnFailure / continueParseOnFailure override
matrix: record-level boolean on @CsvRecord
+ * interacts with field-level tri-state on @DataField so that the field
opinion overrides the record default, and
+ * INHERIT falls back to the record.
+ *
+ * Each model has three fields (id, orderDate, customerName); the bad row
supplies a malformed date string at position
+ * 2. Tolerant outcome: row is delivered, orderDate is null, and customerName
is populated (proving the parser continued
+ * past the bad field). Strict outcome: an exception propagates.
+ */
+public class BindyCsvContinueOnParseFailureTest extends CamelTestSupport {
+
+ private static final String BAD_ROW = "1,not-a-date,Alice";
+ private static final String GOOD_ROW = "1,2026-01-15,Alice";
+
+ @Test
+ public void recordUnset_fieldInherit_isStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:strict-default", BAD_ROW));
+ }
+
+ @Test
+ public void recordTolerant_fieldInherit_keepsRowWithNullField() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:record-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:record-tolerant", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ RecordTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(RecordTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void recordStrict_fieldInherit_isStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:record-strict", BAD_ROW));
+ }
+
+ @Test
+ public void recordTolerant_fieldFalse_fieldOverridesToStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:field-strict-override",
BAD_ROW));
+ }
+
+ @Test
+ public void recordStrict_fieldTrue_fieldOverridesToTolerant() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:field-tolerant-override");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:field-tolerant-override", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ FieldTolerantOverrideOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(FieldTolerantOverrideOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void recordUnset_fieldTrue_keepsRowWithNullField() throws Exception
{
+ MockEndpoint mock = getMockEndpoint("mock:field-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:field-tolerant", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ FieldTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(FieldTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ // ---- defaultValue substitution ----
+
+ @Test
+ public void tolerant_withDefaultValue_substitutesParsedDefault() throws
Exception {
+ // Bad date input + defaultValue set -> field is populated with the
parsed defaultValue (not null)
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-with-default");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:tolerant-with-default", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantWithDefaultValueOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantWithDefaultValueOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ // Substituted from defaultValue="1970-01-01"
+ assertNotNull(row.orderDate);
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
+ assertEquals(fmt.parse("1970-01-01"), row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void strict_withDefaultValue_stillThrows() {
+ // Even if defaultValue is set, strict mode propagates the parse
exception
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:strict-with-default",
BAD_ROW));
+ }
+
+ @Test
+ public void tolerant_malformedDefaultValue_throws() {
+ // defaultValue itself cannot be parsed by the field's format ->
exception propagates
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:tolerant-malformed-default",
BAD_ROW));
+ }
+
+ // ---- primitive / null fallback when no defaultValue is set ----
+
+ @Test
+ public void tolerant_primitiveIntField_getsZero() throws Exception {
+ // Bad int input + tolerant + no defaultValue -> primitive default (0)
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-primitive-int");
+ mock.expectedMessageCount(1);
+ // id at pos 1 is the bad field here; the date is valid
+ template.sendBody("direct:tolerant-primitive-int",
"abc,2026-01-15,Alice");
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantPrimitiveIntOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantPrimitiveIntOrder.class);
+ assertNotNull(row);
+ assertEquals(Integer.MIN_VALUE, row.id); // Bindy's primitive
default convention // primitive int default
+ assertNotNull(row.orderDate); // good input still parses
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void tolerant_multipleBadFields_eachGetsItsOwnFallback() throws
Exception {
+ // Two bad fields on one row:
+ // pos 1 (int) - no defaultValue -> 0
+ // pos 2 (Date) - defaultValue="1970-01-01" -> parsed default
+ // pos 3 (String) is good -> "Alice"
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-multi-bad");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:tolerant-multi-bad", "abc,not-a-date,Alice");
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantMultiBadOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantMultiBadOrder.class);
+ assertNotNull(row);
+ assertEquals(Integer.MIN_VALUE, row.id); // Bindy's primitive
default convention
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
+ assertEquals(fmt.parse("1970-01-01"), row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void tolerant_emptyDefaultValueExplicit_fallsThroughToPrimitive()
throws Exception {
+ // defaultValue="" (explicitly empty) is treated the same as unset ->
primitive default
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-empty-default");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:tolerant-empty-default",
"abc,2026-01-15,Alice");
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantEmptyDefaultOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantEmptyDefaultOrder.class);
+ assertEquals(Integer.MIN_VALUE, row.id); // Bindy's primitive
default convention
+ }
+
+ @Test
+ public void recordTolerant_goodInput_parsesNormally() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:record-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:record-tolerant", GOOD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ RecordTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(RecordTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNotNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:strict-default")
+ .unmarshal(new
BindyCsvDataFormat(StrictDefaultOrder.class))
+ .to("mock:strict-default");
+ from("direct:record-tolerant")
+ .unmarshal(new
BindyCsvDataFormat(RecordTolerantOrder.class))
+ .to("mock:record-tolerant");
+ from("direct:record-strict")
+ .unmarshal(new
BindyCsvDataFormat(RecordStrictOrder.class))
+ .to("mock:record-strict");
+ from("direct:field-strict-override")
+ .unmarshal(new
BindyCsvDataFormat(FieldStrictOverrideOrder.class))
+ .to("mock:field-strict-override");
+ from("direct:field-tolerant-override")
+ .unmarshal(new
BindyCsvDataFormat(FieldTolerantOverrideOrder.class))
+ .to("mock:field-tolerant-override");
+ from("direct:field-tolerant")
+ .unmarshal(new
BindyCsvDataFormat(FieldTolerantOrder.class))
+ .to("mock:field-tolerant");
+ from("direct:tolerant-with-default")
+ .unmarshal(new
BindyCsvDataFormat(TolerantWithDefaultValueOrder.class))
+ .to("mock:tolerant-with-default");
+ from("direct:strict-with-default")
+ .unmarshal(new
BindyCsvDataFormat(StrictWithDefaultValueOrder.class))
+ .to("mock:strict-with-default");
+ from("direct:tolerant-malformed-default")
+ .unmarshal(new
BindyCsvDataFormat(TolerantMalformedDefaultOrder.class))
+ .to("mock:tolerant-malformed-default");
+ from("direct:tolerant-primitive-int")
+ .unmarshal(new
BindyCsvDataFormat(TolerantPrimitiveIntOrder.class))
+ .to("mock:tolerant-primitive-int");
+ from("direct:tolerant-multi-bad")
+ .unmarshal(new
BindyCsvDataFormat(TolerantMultiBadOrder.class))
+ .to("mock:tolerant-multi-bad");
+ from("direct:tolerant-empty-default")
+ .unmarshal(new
BindyCsvDataFormat(TolerantEmptyDefaultOrder.class))
+ .to("mock:tolerant-empty-default");
+ }
+ };
+ }
+
+ @CsvRecord(separator = ",")
+ public static class StrictDefaultOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class RecordTolerantOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = false)
+ public static class RecordStrictOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class FieldStrictOverrideOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", continueParseOnFailure =
ContinueOnFailure.FALSE)
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = false)
+ public static class FieldTolerantOverrideOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", continueParseOnFailure =
ContinueOnFailure.TRUE)
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",")
+ public static class FieldTolerantOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", continueParseOnFailure =
ContinueOnFailure.TRUE)
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class TolerantWithDefaultValueOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", defaultValue =
"1970-01-01")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",")
+ public static class StrictWithDefaultValueOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", defaultValue =
"1970-01-01")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class TolerantMalformedDefaultOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", defaultValue =
"this-is-not-a-date")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class TolerantPrimitiveIntOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class TolerantMultiBadOrder {
+ @DataField(pos = 1)
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", defaultValue =
"1970-01-01")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+
+ @CsvRecord(separator = ",", continueParseOnFailure = true)
+ public static class TolerantEmptyDefaultOrder {
+ @DataField(pos = 1, defaultValue = "")
+ public int id;
+ @DataField(pos = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3)
+ public String customerName;
+ }
+}
diff --git
a/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/fix/BindyKvpContinueOnParseFailureTest.java
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/fix/BindyKvpContinueOnParseFailureTest.java
new file mode 100644
index 000000000000..5bc79b21895e
--- /dev/null
+++
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/fix/BindyKvpContinueOnParseFailureTest.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dataformat.bindy.fix;
+
+import java.util.Date;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.dataformat.bindy.annotation.ContinueOnFailure;
+import org.apache.camel.dataformat.bindy.annotation.KeyValuePairField;
+import org.apache.camel.dataformat.bindy.annotation.Message;
+import org.apache.camel.dataformat.bindy.kvp.BindyKeyValuePairDataFormat;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * KeyValuePair parallel of BindyCsvContinueOnParseFailureTest. @Message
controls the record-level
+ * default; @KeyValuePairField provides the field-level tri-state. Bad value
on tag 2 (orderDate).
+ */
+public class BindyKvpContinueOnParseFailureTest extends CamelTestSupport {
+
+ private static final String BAD_ROW = "1=1 2=not-a-date 3=Alice";
+ private static final String GOOD_ROW = "1=1 2=2026-01-15 3=Alice";
+
+ @Test
+ public void recordUnset_fieldInherit_isStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:strict-default", BAD_ROW));
+ }
+
+ @Test
+ public void recordTolerant_fieldInherit_keepsRowWithNullField() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:record-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:record-tolerant", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ RecordTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(RecordTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void recordStrict_fieldInherit_isStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:record-strict", BAD_ROW));
+ }
+
+ @Test
+ public void recordTolerant_fieldFalse_fieldOverridesToStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:field-strict-override",
BAD_ROW));
+ }
+
+ @Test
+ public void recordStrict_fieldTrue_fieldOverridesToTolerant() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:field-tolerant-override");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:field-tolerant-override", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ FieldTolerantOverrideOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(FieldTolerantOverrideOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void recordUnset_fieldTrue_keepsRowWithNullField() throws Exception
{
+ MockEndpoint mock = getMockEndpoint("mock:field-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:field-tolerant", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ FieldTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(FieldTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void tolerant_primitiveIntField_getsMinValue() throws Exception {
+ // @KeyValuePairField has no defaultValue element today, so the only
fallback for a bad
+ // primitive int is getDefaultValueForPrimitive (which returns
MIN_VALUE in Bindy).
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-primitive-int");
+ mock.expectedMessageCount(1);
+ // tag 1 (int) is bad; tags 2 and 3 are valid
+ template.sendBody("direct:tolerant-primitive-int", "1=abc 2=2026-01-15
3=Alice");
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantPrimitiveIntOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantPrimitiveIntOrder.class);
+ assertNotNull(row);
+ assertEquals(Integer.MIN_VALUE, row.id);
+ assertNotNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Test
+ public void recordTolerant_goodInput_parsesNormally() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:record-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:record-tolerant", GOOD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ RecordTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(RecordTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNotNull(row.orderDate);
+ assertEquals("Alice", row.customerName);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:strict-default")
+ .unmarshal(new
BindyKeyValuePairDataFormat(StrictDefaultOrder.class))
+ .to("mock:strict-default");
+ from("direct:record-tolerant")
+ .unmarshal(new
BindyKeyValuePairDataFormat(RecordTolerantOrder.class))
+ .to("mock:record-tolerant");
+ from("direct:record-strict")
+ .unmarshal(new
BindyKeyValuePairDataFormat(RecordStrictOrder.class))
+ .to("mock:record-strict");
+ from("direct:field-strict-override")
+ .unmarshal(new
BindyKeyValuePairDataFormat(FieldStrictOverrideOrder.class))
+ .to("mock:field-strict-override");
+ from("direct:field-tolerant-override")
+ .unmarshal(new
BindyKeyValuePairDataFormat(FieldTolerantOverrideOrder.class))
+ .to("mock:field-tolerant-override");
+ from("direct:field-tolerant")
+ .unmarshal(new
BindyKeyValuePairDataFormat(FieldTolerantOrder.class))
+ .to("mock:field-tolerant");
+ from("direct:tolerant-primitive-int")
+ .unmarshal(new
BindyKeyValuePairDataFormat(TolerantPrimitiveIntOrder.class))
+ .to("mock:tolerant-primitive-int");
+ }
+ };
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=")
+ public static class StrictDefaultOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=",
continueParseOnFailure = true)
+ public static class RecordTolerantOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=",
continueParseOnFailure = false)
+ public static class RecordStrictOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=",
continueParseOnFailure = true)
+ public static class FieldStrictOverrideOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd",
continueParseOnFailure = ContinueOnFailure.FALSE)
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=",
continueParseOnFailure = false)
+ public static class FieldTolerantOverrideOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd",
continueParseOnFailure = ContinueOnFailure.TRUE)
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=")
+ public static class FieldTolerantOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd",
continueParseOnFailure = ContinueOnFailure.TRUE)
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+
+ @Message(pairSeparator = " ", keyValuePairSeparator = "=",
continueParseOnFailure = true)
+ public static class TolerantPrimitiveIntOrder {
+ @KeyValuePairField(tag = 1)
+ public int id;
+ @KeyValuePairField(tag = 2, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @KeyValuePairField(tag = 3)
+ public String customerName;
+ }
+}
diff --git
a/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/fixed/BindyFixedContinueOnParseFailureTest.java
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/fixed/BindyFixedContinueOnParseFailureTest.java
new file mode 100644
index 000000000000..6c24f15f1337
--- /dev/null
+++
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/fixed/BindyFixedContinueOnParseFailureTest.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dataformat.bindy.fixed;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.dataformat.bindy.annotation.ContinueOnFailure;
+import org.apache.camel.dataformat.bindy.annotation.DataField;
+import org.apache.camel.dataformat.bindy.annotation.FixedLengthRecord;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Fixed-length parallel of BindyCsvContinueOnParseFailureTest. Each model has
id (1 char), orderDate (10 chars,
+ * yyyy-MM-dd pattern), and customerName (10 chars, trimmed). The bad row
supplies "notadate--" at the orderDate offset.
+ */
+public class BindyFixedContinueOnParseFailureTest extends CamelTestSupport {
+
+ private static final String BAD_ROW = "1notadate--Alice ";
+ private static final String GOOD_ROW = "12026-01-15Alice ";
+
+ @Test
+ public void recordUnset_fieldInherit_isStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:strict-default", BAD_ROW));
+ }
+
+ @Test
+ public void recordTolerant_fieldInherit_keepsRowWithNullField() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:record-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:record-tolerant", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ RecordTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(RecordTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName.trim());
+ }
+
+ @Test
+ public void recordStrict_fieldInherit_isStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:record-strict", BAD_ROW));
+ }
+
+ @Test
+ public void recordTolerant_fieldFalse_fieldOverridesToStrict() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:field-strict-override",
BAD_ROW));
+ }
+
+ @Test
+ public void recordStrict_fieldTrue_fieldOverridesToTolerant() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:field-tolerant-override");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:field-tolerant-override", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ FieldTolerantOverrideOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(FieldTolerantOverrideOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName.trim());
+ }
+
+ @Test
+ public void recordUnset_fieldTrue_keepsRowWithNullField() throws Exception
{
+ MockEndpoint mock = getMockEndpoint("mock:field-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:field-tolerant", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ FieldTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(FieldTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNull(row.orderDate);
+ assertEquals("Alice", row.customerName.trim());
+ }
+
+ @Test
+ public void tolerant_withDefaultValue_substitutesParsedDefault() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-with-default");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:tolerant-with-default", BAD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantWithDefaultValueOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantWithDefaultValueOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNotNull(row.orderDate);
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
+ assertEquals(fmt.parse("1970-01-01"), row.orderDate);
+ assertEquals("Alice", row.customerName.trim());
+ }
+
+ @Test
+ public void strict_withDefaultValue_stillThrows() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:strict-with-default",
BAD_ROW));
+ }
+
+ @Test
+ public void tolerant_malformedDefaultValue_throws() {
+ assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:tolerant-malformed-default",
BAD_ROW));
+ }
+
+ @Test
+ public void tolerant_primitiveIntField_getsMinValue() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:tolerant-primitive-int");
+ mock.expectedMessageCount(1);
+ // id at pos 1 is the bad field; date and name are valid
+ template.sendBody("direct:tolerant-primitive-int", "x2026-01-15Alice
");
+ MockEndpoint.assertIsSatisfied(context);
+
+ TolerantPrimitiveIntOrder row
+ =
mock.getReceivedExchanges().get(0).getIn().getBody(TolerantPrimitiveIntOrder.class);
+ assertNotNull(row);
+ assertEquals(Integer.MIN_VALUE, row.id); // Bindy's primitive
default convention
+ assertNotNull(row.orderDate);
+ assertEquals("Alice", row.customerName.trim());
+ }
+
+ @Test
+ public void recordTolerant_goodInput_parsesNormally() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:record-tolerant");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:record-tolerant", GOOD_ROW);
+ MockEndpoint.assertIsSatisfied(context);
+
+ RecordTolerantOrder row =
mock.getReceivedExchanges().get(0).getIn().getBody(RecordTolerantOrder.class);
+ assertNotNull(row);
+ assertEquals(1, row.id);
+ assertNotNull(row.orderDate);
+ assertEquals("Alice", row.customerName.trim());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:strict-default")
+ .unmarshal(new
BindyFixedLengthDataFormat(StrictDefaultOrder.class))
+ .to("mock:strict-default");
+ from("direct:record-tolerant")
+ .unmarshal(new
BindyFixedLengthDataFormat(RecordTolerantOrder.class))
+ .to("mock:record-tolerant");
+ from("direct:record-strict")
+ .unmarshal(new
BindyFixedLengthDataFormat(RecordStrictOrder.class))
+ .to("mock:record-strict");
+ from("direct:field-strict-override")
+ .unmarshal(new
BindyFixedLengthDataFormat(FieldStrictOverrideOrder.class))
+ .to("mock:field-strict-override");
+ from("direct:field-tolerant-override")
+ .unmarshal(new
BindyFixedLengthDataFormat(FieldTolerantOverrideOrder.class))
+ .to("mock:field-tolerant-override");
+ from("direct:field-tolerant")
+ .unmarshal(new
BindyFixedLengthDataFormat(FieldTolerantOrder.class))
+ .to("mock:field-tolerant");
+ from("direct:tolerant-with-default")
+ .unmarshal(new
BindyFixedLengthDataFormat(TolerantWithDefaultValueOrder.class))
+ .to("mock:tolerant-with-default");
+ from("direct:strict-with-default")
+ .unmarshal(new
BindyFixedLengthDataFormat(StrictWithDefaultValueOrder.class))
+ .to("mock:strict-with-default");
+ from("direct:tolerant-malformed-default")
+ .unmarshal(new
BindyFixedLengthDataFormat(TolerantMalformedDefaultOrder.class))
+ .to("mock:tolerant-malformed-default");
+ from("direct:tolerant-primitive-int")
+ .unmarshal(new
BindyFixedLengthDataFormat(TolerantPrimitiveIntOrder.class))
+ .to("mock:tolerant-primitive-int");
+ }
+ };
+ }
+
+ @FixedLengthRecord(length = 21)
+ public static class StrictDefaultOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = true)
+ public static class RecordTolerantOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = false)
+ public static class RecordStrictOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = true)
+ public static class FieldStrictOverrideOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd",
continueParseOnFailure = ContinueOnFailure.FALSE)
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = false)
+ public static class FieldTolerantOverrideOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd",
continueParseOnFailure = ContinueOnFailure.TRUE)
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21)
+ public static class FieldTolerantOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd",
continueParseOnFailure = ContinueOnFailure.TRUE)
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = true)
+ public static class TolerantWithDefaultValueOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd", defaultValue
= "1970-01-01")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21)
+ public static class StrictWithDefaultValueOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd", defaultValue
= "1970-01-01")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = true)
+ public static class TolerantMalformedDefaultOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd", defaultValue
= "this-is-not-a-date")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+
+ @FixedLengthRecord(length = 21, continueParseOnFailure = true)
+ public static class TolerantPrimitiveIntOrder {
+ @DataField(pos = 1, length = 1)
+ public int id;
+ @DataField(pos = 2, length = 10, pattern = "yyyy-MM-dd")
+ public Date orderDate;
+ @DataField(pos = 3, length = 10, trim = true)
+ public String customerName;
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index 982c47367530..f75c5e87f73a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -497,6 +497,42 @@ Hazelcast topic, queue, map, list, set, or in one of the
repositories
above must provide their own `Config` with a
`JavaSerializationFilterConfig` configured for their class names.
+=== camel-bindy
+
+Bindy can now keep going when an individual field fails to parse, instead of
aborting the whole
+unmarshal. Two new opt-in annotation elements drive this:
+
+* `continueParseOnFailure` on `@CsvRecord`, `@FixedLengthRecord`, and
`@Message` (record-level boolean,
+ default `false`). When `true`, every field on the record tolerates parse
failures by default.
+* `continueParseOnFailure` on `@DataField` and `@KeyValuePairField`
(field-level tri-state of type
+ `ContinueOnFailure`, default `INHERIT`). `TRUE` and `FALSE` override the
record-level setting
+ for that one field; `INHERIT` defers to the record.
+
+When a parse fails and the field is tolerant, Bindy substitutes the field's
existing `@DataField.defaultValue`
+(if set) by running it through the same `Format` instance — so the substitute
is automatically the
+right Java type. If no `defaultValue` is set, the field gets the same fallback
an unfilled field would
+get today: `null` for object types, and the primitive's `MIN_VALUE` sentinel
(the existing
+`getDefaultValueForPrimitive` convention) for `int`, `long`, etc. `boolean`
falls back to `false` and
+`String` to `""` (or `null` if `defaultValueStringAsNull` is enabled on the
DataFormat).
+
+Everything is opt-in; default behavior is unchanged. `@KeyValuePairField` does
not have a `defaultValue`
+element today, so KVP fields can only fall back to the type-appropriate
default.
+
+[source,java]
+----
+@CsvRecord(separator = ",", continueParseOnFailure = true)
+public class Order {
+ @DataField(pos = 1)
+ private int id;
+
+ @DataField(pos = 2, pattern = "yyyy-MM-dd", defaultValue = "1970-01-01")
+ private Date orderDate; // bad input -> 1970-01-01 instead of throwing
+
+ @DataField(pos = 3, continueParseOnFailure = ContinueOnFailure.FALSE)
+ private BigDecimal amount; // this one field stays strict
+}
+----
+
=== camel-keycloak
The `KeycloakSecurityPolicy` route policy now always verifies the access token
when one is present - signature,