This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch 
WW-2934-skip-validators-on-conversion-error
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 4ce1690dd7eb25997f9fba15d661f978a991826b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Thu Jul 23 21:32:57 2026 +0200

    WW-2934 feat(core): skip field validators on conversion error behind opt-in 
flag
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../java/org/apache/struts2/StrutsConstants.java   |  3 ++
 .../validator/DefaultActionValidatorManager.java   | 14 ++++++
 .../org/apache/struts2/default.properties          |  5 ++
 .../validator/ConversionErrorSkipAction.java       | 48 ++++++++++++++++++
 .../DefaultActionValidatorManagerTest.java         | 57 ++++++++++++++++++++++
 .../ConversionErrorSkipAction-validation.xml       | 43 ++++++++++++++++
 6 files changed, 170 insertions(+)

diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java 
b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index e76fac4e5..7530bd307 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -419,6 +419,9 @@ public final class StrutsConstants {
      */
     public static final String STRUTS_ACTIONVALIDATORMANAGER = 
"struts.actionValidatorManager";
 
+    /** @see org.apache.struts2.validator.DefaultActionValidatorManager */
+    public static final String 
STRUTS_VALIDATORS_SKIP_VALIDATORS_ON_CONVERSION_ERROR = 
"struts.validators.skipValidatorsOnConversionError";
+
     /**
      * The {@link org.apache.struts2.util.ValueStackFactory} implementation 
class
      */
diff --git 
a/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
 
b/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
index 2f2b758b7..f5c40a1fb 100644
--- 
a/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
+++ 
b/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
@@ -25,6 +25,7 @@ import org.apache.struts2.text.TextProviderFactory;
 import org.apache.struts2.inject.Inject;
 import org.apache.struts2.util.ClassLoaderUtil;
 import org.apache.struts2.util.ValueStack;
+import org.apache.struts2.validator.validators.ConversionErrorFieldValidator;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsConstants;
@@ -75,6 +76,7 @@ public class DefaultActionValidatorManager implements 
ActionValidatorManager {
     protected ValidatorFileParser validatorFileParser;
     protected FileManager fileManager;
     protected boolean reloadingConfigs;
+    protected boolean skipValidatorsOnConversionError;
     protected TextProviderFactory textProviderFactory;
 
     @Inject
@@ -97,6 +99,11 @@ public class DefaultActionValidatorManager implements 
ActionValidatorManager {
         this.reloadingConfigs = Boolean.parseBoolean(reloadingConfigs);
     }
 
+    @Inject(value = 
StrutsConstants.STRUTS_VALIDATORS_SKIP_VALIDATORS_ON_CONVERSION_ERROR, required 
= false)
+    public void setSkipValidatorsOnConversionError(String 
skipValidatorsOnConversionError) {
+        this.skipValidatorsOnConversionError = 
Boolean.parseBoolean(skipValidatorsOnConversionError);
+    }
+
     @Inject
     public void setTextProviderFactory(TextProviderFactory 
textProviderFactory) {
         this.textProviderFactory = textProviderFactory;
@@ -183,6 +190,13 @@ public class DefaultActionValidatorManager implements 
ActionValidatorManager {
                     LOG.debug("Short-circuited, skipping");
                     continue;
                 }
+
+                if (skipValidatorsOnConversionError
+                        && !(validator instanceof 
ConversionErrorFieldValidator)
+                        && 
ActionContext.getContext().getConversionErrors().containsKey(fullFieldName)) {
+                    LOG.debug("Skipping validator {} for field {} due to a 
conversion error", validator, fullFieldName);
+                    continue;
+                }
             }
 
             if (validator instanceof ShortCircuitableValidator && 
((ShortCircuitableValidator) validator).isShortCircuit()) {
diff --git a/core/src/main/resources/org/apache/struts2/default.properties 
b/core/src/main/resources/org/apache/struts2/default.properties
index 1c501cb33..0dcb5ab80 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -146,6 +146,11 @@ struts.mapper.action.prefix.crossNamespaces = false
 ###                them right away.
 struts.devMode = false
 
+### When set to true, a field's remaining validators are skipped once that 
field
+### has a type conversion error, avoiding a duplicate error (WW-2934).
+### valid values are: true, false (false is the default)
+struts.validators.skipValidatorsOnConversionError = false
+
 ### when set to true, resource bundles will be reloaded on _every_ request.
 ### this is good during development, but should never be used in production
 # struts.i18n.reload=false
diff --git 
a/core/src/test/java/org/apache/struts2/validator/ConversionErrorSkipAction.java
 
b/core/src/test/java/org/apache/struts2/validator/ConversionErrorSkipAction.java
new file mode 100644
index 000000000..e9b22fc3e
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/validator/ConversionErrorSkipAction.java
@@ -0,0 +1,48 @@
+/*
+ * 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.struts2.validator;
+
+import org.apache.struts2.ActionSupport;
+
+/**
+ * Fixture for WW-2934: an Integer field ("age") that carries both a conversion
+ * validator and a required validator, plus an unrelated required String field
+ * ("name") and an action-level validator (see the matching -validation.xml).
+ */
+public class ConversionErrorSkipAction extends ActionSupport {
+
+    private Integer age;
+    private String name;
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}
diff --git 
a/core/src/test/java/org/apache/struts2/validator/DefaultActionValidatorManagerTest.java
 
b/core/src/test/java/org/apache/struts2/validator/DefaultActionValidatorManagerTest.java
index 06914d31f..f2ad1550f 100644
--- 
a/core/src/test/java/org/apache/struts2/validator/DefaultActionValidatorManagerTest.java
+++ 
b/core/src/test/java/org/apache/struts2/validator/DefaultActionValidatorManagerTest.java
@@ -18,11 +18,13 @@
  */
 package org.apache.struts2.validator;
 
+import org.apache.struts2.ActionContext;
 import org.apache.struts2.FileManagerFactory;
 import org.apache.struts2.SimpleAction;
 import org.apache.struts2.TestBean;
 import org.apache.struts2.ValidationOrderAction;
 import org.apache.struts2.XWorkTestCase;
+import org.apache.struts2.conversion.impl.ConversionData;
 import org.apache.struts2.interceptor.ValidationAware;
 import org.apache.struts2.test.DataAware2;
 import org.apache.struts2.test.SimpleAction3;
@@ -375,4 +377,59 @@ public class DefaultActionValidatorManagerTest extends 
XWorkTestCase {
         assertEquals((e.getValue()).get(0), "password hint is required");
     }
 
+    public void testConversionError_bothErrorsWhenFlagDisabledByDefault() 
throws Exception {
+        ConversionErrorSkipAction action = new ConversionErrorSkipAction();
+        ActionContext.getContext().getConversionErrors()
+                .put("age", new ConversionData(new String[]{"one"}, 
Integer.class));
+
+        actionValidatorManager.validate(action, null);
+
+        List<String> ageErrors = action.getFieldErrors().get("age");
+        assertNotNull(ageErrors);
+        assertEquals(2, ageErrors.size()); // conversion + required, current 
behavior
+        assertTrue(ageErrors.contains("Age must be a valid number"));
+        assertTrue(ageErrors.contains("Age is required"));
+    }
+
+    public void testConversionError_fieldValidatorsSkippedWhenEnabled() throws 
Exception {
+        ConversionErrorSkipAction action = new ConversionErrorSkipAction();
+        ActionContext.getContext().getConversionErrors()
+                .put("age", new ConversionData(new String[]{"one"}, 
Integer.class));
+        actionValidatorManager.setSkipValidatorsOnConversionError("true");
+
+        actionValidatorManager.validate(action, null);
+
+        List<String> ageErrors = action.getFieldErrors().get("age");
+        assertNotNull(ageErrors);
+        // required is skipped; the conversion validator itself still runs
+        assertEquals(1, ageErrors.size());
+        assertEquals("Age must be a valid number", ageErrors.get(0));
+    }
+
+    public void testConversionError_unrelatedFieldStillValidatedWhenEnabled() 
throws Exception {
+        ConversionErrorSkipAction action = new ConversionErrorSkipAction();
+        ActionContext.getContext().getConversionErrors()
+                .put("age", new ConversionData(new String[]{"one"}, 
Integer.class));
+        actionValidatorManager.setSkipValidatorsOnConversionError("true");
+
+        actionValidatorManager.validate(action, null);
+
+        List<String> nameErrors = action.getFieldErrors().get("name");
+        assertNotNull(nameErrors); // "name" has no conversion error, still 
validated
+        assertEquals(1, nameErrors.size());
+        assertEquals("Name is required", nameErrors.get(0));
+    }
+
+    public void 
testConversionError_actionLevelValidatorUnaffectedWhenEnabled() throws 
Exception {
+        ConversionErrorSkipAction action = new ConversionErrorSkipAction();
+        ActionContext.getContext().getConversionErrors()
+                .put("age", new ConversionData(new String[]{"one"}, 
Integer.class));
+        actionValidatorManager.setSkipValidatorsOnConversionError("true");
+
+        actionValidatorManager.validate(action, null);
+
+        assertTrue(action.hasActionErrors());
+        assertTrue(action.getActionErrors().contains("Action level always 
fails"));
+    }
+
 }
diff --git 
a/core/src/test/resources/org/apache/struts2/validator/ConversionErrorSkipAction-validation.xml
 
b/core/src/test/resources/org/apache/struts2/validator/ConversionErrorSkipAction-validation.xml
new file mode 100644
index 000000000..e8a6af67e
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/validator/ConversionErrorSkipAction-validation.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.2//EN" 
"https://struts.apache.org/dtds/xwork-validator-1.0.2.dtd";>
+<validators>
+    <field name="age">
+        <field-validator type="conversion">
+            <message>Age must be a valid number</message>
+        </field-validator>
+        <field-validator type="required">
+            <message>Age is required</message>
+        </field-validator>
+    </field>
+
+    <field name="name">
+        <field-validator type="required">
+            <message>Name is required</message>
+        </field-validator>
+    </field>
+
+    <validator type="expression">
+        <param name="expression">false</param>
+        <message>Action level always fails</message>
+    </validator>
+</validators>

Reply via email to