Bean Validation Plugin
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Struts 7.4.0 (released 2026-10-02) with Hibernate Validator 9.1.4.Final as the Bean
Validation provider, running on Apache Tomcat 11.0.26 and Java 21; the facts were checked on 2026-10-08 against the
official plugin page, the source of the plugin in the 7.4.0 sources jar, the contents of the plugin’s struts-plugin.xml
and the Hibernate Validator reference guide, and every example was built into a Maven WAR, deployed to the scratch Tomcat
and requested with curl. Struts has had its own validation framework since version 2.0 (the validation interceptor,
-validation.xml files, validate() and the field validators described in Validation). The
Bean Validation plugin, available since Struts 2.5, offers an alternative: instead of Struts validators, the rules are
Jakarta Validation annotations (@NotBlank, @Size, @Min, …) written on the fields of the action or of its model,
evaluated by a provider such as Hibernate Validator, with the violations turned into the same field errors and action
errors that the workflow interceptor and the s:fielderror tag already understand. The running scenario is the
Bookshelf, a catalogue of books; this page validates the "add a book" form with annotations, moves the rules to a
model object with a nested publisher, adds a validation group that applies only when a book is published and a
class-level constraint that compares two e-mail fields, shows how the messages are resolved and why they do not follow
the Struts locale, and ends with the mistakes that fail either the start-up or the validation without a clear message.
The pages on type conversion, parameter injection and
localization cover the neighbouring topics and are linked where they come up.
What the Plugin Is
The plugin is a thin bridge, a single small jar with about ten classes. The official page says it "implements a bridge to use Bean Validation in struts actions" and lists four features it integrates with: i18n, model driven actions, AJAX validation and the workflow interceptor. Everything else (the constraint annotations, the validation engine, the message interpolation, the groups) belongs to Jakarta Validation and to the provider, which is why the page that defines the constraints in detail is the Hibernate Validator reference, not the Struts documentation. The table lists the parts that the jar contains in 7.4.0.
| Part | Role |
|---|---|
|
Maven artifact |
|
A package declared by the plugin’s |
|
The interceptor registered under the name |
|
A singleton bean that reads the constants, builds the |
|
Method annotation |
|
Class-level constraint |
|
The annotation of core, |
|
An interceptor stack that is the default stack of the |
The plugin is a Struts plugin in the usual sense (a jar with a struts-plugin.xml at its root that the framework loads
from the class path, see Configuration: struts.xml & Constants), so adding the jar to the application is enough to make
the package and the interceptor known. Nothing is validated until an action is configured in a package that extends
struts-bean-validation (or in a package with its own stack that names the beanValidation interceptor).
Setup: The Plugin, a Provider and the Servlet Container
Two dependencies are needed. The plugin is the Struts half; the Jakarta Validation provider is the half that does the
work, and the plugin does not bring one. The Maven Central coordinates used in the scenario are the following; with
struts2-bom 7.4.0 imported, the Struts artifacts need no version.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-bom</artifactId>
<version>7.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-bean-validation-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.1.4.Final</version>
</dependency>
</dependencies>
A few facts about this set of artifacts, all observed in the scratch WAR (mvn dependency:tree and the contents of
WEB-INF/lib):
-
Hibernate Validator 9.1.4.Final brings
jboss-loggingandclassmateand needs an Expression Language implementation to interpolate messages such as${validatedValue}. Tomcat 11 providesjakarta.elin its own class path, so a WAR on Tomcat needs nothing more; a servlet container without EL (Jetty, for instance) needs ajakarta.elimplementation declared by the application. -
The plugin’s only dependency is
jakarta.validation-api3.0.2, and thestruts2-bommanages that version, while Hibernate Validator 9.x implements Jakarta Validation 3.1. Maven resolves the managed 3.0.2 jar, and the scenario of this page (annotations fromjakarta.validation.constraints, groups,@Valid, a class-level constraint) ran without a problem on that combination. Application code that uses a 3.1 feature would need the 3.1 API jar declared explicitly. -
The namespace is
jakarta.validation, neverjavax.validation: Struts 7 is a Jakarta EE 10/11 framework, and Hibernate Validator 9 only knows the Jakarta namespace. The official plugin page still showsjavax.validationimports; see the section on outdated material below. -
The provider is found through the Java service loader (
META-INF/services/jakarta.validation.spi.ValidationProvider, which Hibernate Validator’s jar contains), unless a constant names the class.
Configuration: the Package, the Constants and the Stack
The configuration has two parts: the package that the application’s actions live in, and four constants. The package declaration is the part that is always needed.
<struts>
<constant name="struts.beanValidation.providerClass" value="org.hibernate.validator.HibernateValidator"/>
<constant name="struts.beanValidation.ignoreXMLConfiguration" value="true"/>
<constant name="struts.allowlist.packageNames" value="org.example.bookshelf.model"/>
<package name="bv" extends="struts-bean-validation" namespace="/bv">
<global-results>
<result name="403">/WEB-INF/views/forbidden.jsp</result>
</global-results>
<action name="add" class="org.example.bookshelf.actions.AddBookAction">
<result name="input">/WEB-INF/views/form.jsp</result>
<result>/WEB-INF/views/ok.jsp</result>
</action>
</package>
</struts>
The package element is the one that matters: extends="struts-bean-validation" makes the package inherit the
beanValidation interceptor and, more importantly, the beanValidationDefaultStack that replaces defaultStack as the
default interceptor reference. The global-results entry maps the result name 403, which the fetchMetadata
interceptor returns for rejected cross-site requests (explained in its own section below). The struts.allowlist...
constant is a 7.x requirement of the OGNL allowlist that the model classes of the scenario need; the validation itself
does not use OGNL. The four constants of the plugin, whose defaults are declared in the plugin’s struts-plugin.xml,
are the following.
| Constant | Default | Effect |
|---|---|---|
|
empty |
The class name of the provider ( |
|
|
When |
|
|
When |
|
|
The encoding of the first decoding step of the previous constant |
The stack that the package makes the default is, in the order of struts-plugin.xml 7.4.0 (the classic validation
interceptor does not appear):
| # | Interceptor | Remark |
|---|---|---|
1 |
|
As in |
2 |
|
Bring the model of a |
3 |
|
Request preprocessing |
4 |
|
Parameter injection and type conversion (see Parameter Injection and @StrutsParameter and Type Conversion); after these the action holds the request values |
5 |
|
Security headers and resource isolation, added in 7.4.0 (WW-5718) |
6 |
|
The plugin’s interceptor, run after the values are in the action |
7 |
|
Returns |
8 |
|
Developer tool |
The diagram shows why the order of the stack matters. The beanValidation interceptor does not stop the request:
it only adds errors to the action, and the workflow interceptor right after it decides, exactly as it does for classic
validation, whether the action method runs or the input result is returned. Because Bean Validation reads the values
that are already in the action, it must come after params and conversionError; the next section shows what
happens when it does not.
What the beanValidation Interceptor Does
The behaviour below is read from the 7.4.0 source of BeanValidationInterceptor and confirmed with the scratch WAR.
-
It asks the
BeanValidationManagerfor aValidator. If there is none, validation is skipped with a debug message. -
It resolves the action method that is going to run (
ActionProxy.getMethod()). When that method carries@SkipValidation, the interceptor does nothing. Otherwise it looks for@ValidationGroupon the same method; without it, the group list isDefaultonly. -
It validates the action or, when the action implements
ModelDriven, the model returned bygetModel(), with those groups. -
For every
ConstraintViolationit takes the interpolated message, looks it up as a Struts text key (next section) and stores the result. A violation whose leaf bean is the invalid value itself (what a class-level constraint produces) becomes an action error; any other violation becomes a field error on the property path (title,publisher.name). For aModelDrivenaction, a leadingmodel.is removed from the field name, so the field error is namedtitle, matching the form field. -
It always lets the invocation continue (
invocation.invoke()); theworkflowinterceptor that follows acts on the errors.
Because the interceptor extends MethodFilterInterceptor, the includeMethods and excludeMethods parameters work as for
other interceptors (see Interceptors); the plugin’s stack sets excludeMethods to
input,back,cancel,browse, which means that an action method with one of those four names is not validated even without
@SkipValidation. The two filters are different: excludeMethods matches the method name configured for the action,
@SkipValidation is an annotation on the method itself.
The Bookshelf Form with Annotations on the Action
The first example puts the constraints on the action, the same way the field validators of the classic framework would be
declared for the action. The action extends ActionSupport, the setters carry @StrutsParameter (the 7 requirement for
fields that the request may set), and the method that only shows the form is marked @SkipValidation.
package org.example.bookshelf.actions;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import org.apache.struts2.interceptor.validation.SkipValidation;
public class AddBookAction extends ActionSupport {
@NotBlank(message = "book.title.required")
@Size(max = 40)
private String title;
@NotBlank
private String author;
@NotNull
@Min(1450)
@Max(2100)
private Integer year;
public String getTitle() { return title; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
@StrutsParameter public void setAuthor(String author) { this.author = author; }
public Integer getYear() { return year; }
@StrutsParameter public void setYear(Integer year) { this.year = year; }
@SkipValidation
@Override
public String input() { return INPUT; }
@Override
public String execute() { return SUCCESS; }
}
The result page lists the action errors and the field errors with plain s:iterator, so that the curl output below
shows the keys and messages exactly. Requested with an empty form, a form with long title and out-of-range year, a form
with a non-numeric year and a valid form, the scenario answered (HTTP 200 for each; the lists are shown in the order
returned, which is the iteration order of a set and not guaranteed):
| Request body | Messages on the page |
|---|---|
|
|
45-character title, |
|
|
|
|
No errors: the |
Four things are visible in this output. The English texts (must not be blank, size must be between 0 and 40) are
the provider’s own messages: nothing needed to be written for them. The title message is the Struts text, because the
annotation names a key (explained in the next section). The empty year field became null (Struts converts an empty
string for a wrapper type to null), so @NotNull produced a violation, not a conversion error. And the non-numeric
year shows two messages on one field: the conversionError interceptor added Invalid field value for field "year".
first, then @NotNull added its message because the property stayed null. The second message is true but confusing;
the usual remedy is to define a text for the conversion error in the action’s bundle (invalid.fieldvalue.year) and
to accept the second message, or to use a wrapper-friendly rule such as @Min instead of @NotNull where only the range matters.
The action that only displays the form is the second use of @SkipValidation. The scratch app maps it with
method="input":
<action name="addform" class="org.example.bookshelf.actions.AddBookAction" method="input">
<result name="input">/WEB-INF/views/form.jsp</result>
</action>
A POST of an empty form to addform.action rendered the form with no errors, while the same request to add.action
listed three. In this particular case the stack’s excludeMethods=input,... would have skipped validation anyway;
@SkipValidation is needed for a method with another name (a prepareForm() or list(), for example).
Messages: Struts Keys, Provider Messages and Locales
Every violation has a message, and the plugin decides what text the user sees in two steps. Understanding them is the most important part of using the plugin in an application that has more than one language.
-
The provider interpolates the message of the annotation. The default messages come from the provider’s bundles (
must not be blank). A message in braces,{book.isbn.pattern}, is looked up by the provider inValidationMessages.propertieson the class path (and in the locale variants of that bundle). Text that the provider cannot resolve is left as written. -
In 7.4.0 the plugin takes the result of step 1 and calls
getText(...)on it through the action’s text provider, that is, it treats the interpolated string as a Struts message key. If a key of that name exists in the bundles that the action sees (AddBookAction.properties, the package and global bundles; see the resource bundle search order in Localization and i18n), the Struts text is used; if not, the string itself is used.
The consequence is that there are two ways to write a message, and they differ in who looks the text up. The scenario
uses both, side by side, on the model class Book of the next section:
@NotBlank(message = "book.title.required")
@Size(max = 40, message = "book.title.tooLong")
private String title;
@NotBlank(groups = Publish.class, message = "book.isbn.required")
@Pattern(regexp = "\\d{10}|\\d{13}", message = "{book.isbn.pattern}")
private String isbn;
With BookModelAction.properties (the bundle of the action) holding
book.title.required=Title is required (from BookModelAction.properties)
book.title.tooLong=Title is longer than 40 characters
book.isbn.required=ISBN is required to publish
publisher.name.required=Publisher name is required
and ValidationMessages.properties at the root of the class path holding book.isbn.pattern=An ISBN has 10 or 13
digits (ValidationMessages.properties), a submission with an empty title and the ISBN 12ab showed title: Title is
required (from BookModelAction.properties) and isbn: An ISBN has 10 or 13 digits (ValidationMessages.properties). The
first message is a plain key that the provider left alone and Struts resolved; the second is a brace expression that the
provider resolved from its own bundle before Struts saw it. Writing the brace key without the provider bundle does not
work: the same {book.isbn.pattern} with the text only in the Struts bundle displayed the literal {book.isbn.pattern}
(observed with the text only in a Struts bundle on the model class, Book.properties), because the provider leaves an unknown brace key as written
and a key with braces is not a name in a Struts bundle.
The locale is where the two paths part. The Struts locale comes from the i18n interceptor (the request_locale
parameter, the session, Accept-Language); the provider’s locale is chosen by Hibernate Validator’s locale resolver,
which by default is the JVM’s default locale. The same bad submission, sent with request_locale=es after adding
BookModelAction_es.properties with book.title.required=El titulo es obligatorio (...), produced the Spanish text for the
title (resolved by Struts) and the English must not be blank and An ISBN has 10 or 13 digits (ValidationMessages.properties)
(resolved by the provider, in spite of a ValidationMessages_es.properties on the class path). An Accept-Language: es
header changed nothing either. For an application that is localized, the choices are: use Struts keys for every message
(so that all texts follow the Struts locale and live in the bundles the rest of the application uses), or configure the
provider’s locale resolver with the application’s locale source through Configuration code in a custom
BeanValidationManager (the manager is an injectable bean, so it can be replaced in struts.xml with a bean element of
type="org.apache.struts.beanvalidation.validation.interceptor.BeanValidationManager"). The first choice needs no code.
The two convertMessage... constants belong to this step. They apply to the message after getText returned it from a
Struts bundle, so they matter only for a project whose .properties files were saved in ISO-8859-1 while holding UTF-8
bytes; a project with correctly encoded bundles leaves both constants at their defaults.
ModelDriven Actions, Nested Beans and Groups
Most Struts applications put the data in a model object rather than in the action, with ModelDriven. The plugin
supports it directly: the model, not the action, is validated. The nested @Valid and the group in the example are
ordinary Jakarta Validation. The model of the scenario is a Book with a nested Publisher.
public class Book {
@NotBlank(message = "book.title.required")
@Size(max = 40, message = "book.title.tooLong")
private String title;
@NotBlank
private String author;
@NotNull
@Min(1450)
@Max(2100)
private Integer year;
@NotBlank(groups = Publish.class, message = "book.isbn.required")
@Pattern(regexp = "\\d{10}|\\d{13}", message = "{book.isbn.pattern}")
private String isbn;
@Valid
private Publisher publisher = new Publisher();
public String getTitle() { return title; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
@StrutsParameter public void setAuthor(String author) { this.author = author; }
public Integer getYear() { return year; }
@StrutsParameter public void setYear(Integer year) { this.year = year; }
public String getIsbn() { return isbn; }
@StrutsParameter public void setIsbn(String isbn) { this.isbn = isbn; }
@StrutsParameter(depth = 1) public Publisher getPublisher() { return publisher; }
public void setPublisher(Publisher publisher) { this.publisher = publisher; }
}
public class Publisher {
@NotBlank(message = "publisher.name.required")
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
/** Validation group: extra rules that apply only when a book is published. */
public interface Publish { }
The action implements ModelDriven<Book> and has two methods, one validated with the default group and one with the
default group plus Publish:
public class BookModelAction extends ActionSupport implements ModelDriven<Book> {
private final Book book = new Book();
@Override
public Book getModel() { return book; }
@Override
public String execute() { return SUCCESS; }
@ValidationGroup({Default.class, Publish.class})
public String publish() { return SUCCESS; }
}
with the mappings model (method execute) and publish (method="publish") in the bv package. The observed
behaviour:
| Request | Result |
|---|---|
|
|
|
|
|
|
|
|
|
|
Four rules follow. Constraints without a groups attribute belong to Default, and only the groups listed by
@ValidationGroup on the running method are validated, so a group’s constraints are silent until a method asks for the
group; the annotation on the method replaces Default, which is why the publish() method lists Default.class
explicitly (without it only the Publish constraints would run). Cascading into the publisher needs @Valid; without
it the nested bean is not validated. The path of a nested violation is the field name that goes into the form
(publisher.name), the same name the s:textfield name="publisher.name" tag uses, so field errors appear next to the
right control. And the @StrutsParameter(depth = 1) annotation on getPublisher() is the Struts 7 rule that lets the
request set publisher.name; it is unrelated to validation, but forgetting it leaves the nested name empty and produces a
confusing Publisher name is required for a form that did submit the field (see Parameter Injection and @StrutsParameter).
A Class-Level Constraint: @FieldMatch
Some rules compare two properties, and a single-field annotation cannot express them. The plugin ships one such
constraint, @FieldMatch, for confirmation fields. It is applied to the class and names the two properties.
@FieldMatch(first = "email", second = "confirmEmail", message = "The two e-mail addresses must match")
public class ReaderAction extends ActionSupport {
@NotBlank @Email
private String email;
private String confirmEmail;
public String getEmail() { return email; }
@StrutsParameter public void setEmail(String email) { this.email = email; }
public String getConfirmEmail() { return confirmEmail; }
@StrutsParameter public void setConfirmEmail(String confirmEmail) { this.confirmEmail = confirmEmail; }
@Override
public String execute() { return SUCCESS; }
}
The scratch results:
| Request body | Messages |
|---|---|
The action error |
|
|
|
|
|
|
|
The mismatch shows up as an action error because a violation raised on the class has the bean itself as its invalid
value; the interceptor files such a violation under actionErrors, so a page must show the action errors (the
s:actionerror tag) as well as the field errors, or the user sees a form that is rejected without a reason. Any custom class-level
constraint written with the Jakarta Validation @Constraint(validatedBy = ...) mechanism behaves the same way; Hibernate
Validator’s own class-level @ScriptAssert does too. A custom constraint that must be tied to a field instead builds the
violation with context.buildConstraintViolationWithTemplate(...).addPropertyNode("confirmEmail"), which makes the path
point at the field and the message a field error.
The Security Interceptors of the Stack (7.4.0)
The plugin’s page for 7.4.0 and 6.12.0 states that beanValidationDefaultStack now contains coep, coop and
fetchMetadata, configured as in the core defaultStack (WW-5718), and that earlier versions left them out. The
consequence is visible in the scenario: the response of a normal request to a plugin package carries the headers
Cross-Origin-Embedder-Policy-Report-Only: require-corp and Cross-Origin-Opener-Policy: same-origin, and fetchMetadata
inspects the Sec-Fetch-* request headers. A cross-site request that is neither a GET nor a top-level navigation is
rejected: the action does not run and the interceptor returns the result code 403, which needs a result named 403 to
display a page. In the scratch app the global result of the bv package maps it:
Request headers (POST of a valid book to bv/add.action) |
Outcome |
|---|---|
|
HTTP 200 with |
|
|
|
|
|
HTTP 404: the unmapped result is reported as "no result defined", not as 403 |
The plugin page says to set fetchMetadata.disabled to true on the stack for actions that must accept cross-site
posts (a public endpoint called from other origins); the rule and its consequences are the topic of the security pages of
this section. The practical point for this page is that a team that migrates a package from struts-default to
struts-bean-validation while updating to 7.4.0 gets these three interceptors without asking for them, so the 403
result must exist in the package or in the parent package.
Combining with Classic Validation, and Writing Your Own Stack
The plugin’s stack deliberately omits the classic validation interceptor, so in a struts-bean-validation package
validate(), validateXxx() and Action-validation.xml files are not run. That is the first thing to know when
migrating an action: a rule that exists only in -validation.xml silently stops being enforced. The scratch app has a
MixedAction with an annotation (@NotBlank title) and a classic validate() method that rejects an ISBN that
is not 10 digits, mapped in the bv package:
public class MixedAction extends ActionSupport {
@NotBlank
private String title;
private String isbn;
// getters and @StrutsParameter setters as in AddBookAction
@Override
public void validate() {
if (isbn == null || !isbn.matches("\\d{10}")) {
addFieldError("isbn", "classic validate(): ISBN must have 10 digits");
}
}
@Override
public String execute() { return SUCCESS; }
}
Requested in the bv package (beanValidationDefaultStack) with title=&isbn=1, only title: must not be blank was
reported: the validate() rule never ran. To use both, a package declares its own stack. The official page has an
example of this, but its XML is not valid and its order is wrong (see the outdated section); the version that was
verified is:
<package name="mix" extends="struts-default" namespace="/mix">
<interceptors>
<interceptor name="beanValidation"
class="org.apache.struts.beanvalidation.validation.interceptor.BeanValidationInterceptor"/>
<interceptor-stack name="beanAfterParamsStack">
<interceptor-ref name="basicStack"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="beanValidation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
</interceptor-stack>
</interceptors>
<action name="both" class="org.example.bookshelf.actions.MixedAction">
<interceptor-ref name="beanAfterParamsStack"/>
<result name="input">/WEB-INF/views/form.jsp</result>
<result>/WEB-INF/views/ok.jsp</result>
</action>
</package>
Requested with title=&isbn=1, mix/both.action showed both isbn: classic validate(): ISBN must have 10 digits and
title: must not be blank; with title=Dune&isbn=1 only the classic message; with a valid pair OK. The stack uses
basicStack (the parameter-handling half of defaultStack) and then the three validation-related interceptors in the order
validators, Bean Validation, workflow. Two warnings go with it. The workflow interceptor must be last: it is
the one that turns the collected errors into the input result. And basicStack leaves out the stack’s other members
(i18n, the model-driven and chain interceptors, the security ones), so a real stack lists what the application needs.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
The application does not start and every URL answers 404; the log shows "Unable to load configuration … Caught Exception while registering Interceptor class …BeanValidationInterceptor" and |
The plugin is on the class path but there is no Jakarta Validation provider; the manager builds the factory at start-up, which fails (verified by removing Hibernate Validator from the WAR; |
Add |
The action is validated although |
A hand-written stack puts |
Put |
|
The plugin stack has no |
Move the rules to annotations, or use a stack with both interceptors (previous section) |
The form is rejected but no message is displayed |
A class-level constraint produced an action error; the page only shows field errors |
Render |
Messages stay English when the user chose another language |
The provider uses the JVM default locale, not the Struts locale |
Use Struts keys for the messages ( |
|
A brace key is resolved by the provider, from |
Put the text in |
A group’s constraints never fire |
The running method has no |
Annotate the method with |
A nested bean is not validated |
No |
Add |
A field is reported twice ( |
The |
Define the conversion-error text in the action bundle, or drop the redundant rule |
|
The annotation is on the class, or on a method that is not the action method configured with |
Annotate the action method itself |
Cross-site POST answers 404 after the update to 7.4.0 |
|
Add a global |
|
Copied from the official example or from an old book |
Use |
What Changed Recently
| Version | Change |
|---|---|
Struts 2.5 |
The plugin was introduced; classic validation stays available and the validation page of the core guide points to the plugin as the annotation alternative |
Struts 7.0 |
The plugin moved to the Jakarta namespace ( |
Struts 7.0 (security) |
The |
Struts 7.4.0 and 6.12.0 |
|
Struts 7.5.0 and 6.13.0 (announced on the plugin page; not in 7.4.0) |
WW-5757: the plugin looks up the annotation’s |
Hibernate Validator 9 |
Implements Jakarta Validation 3.1; the legacy |
Source: Apache JIRA — WW-5718
Source: Apache JIRA — WW-5757
Where the Official Page and the Older Books Are Outdated
The sources of this page are the official plugin page, the Struts 2 books of the OVal/validation era and Hibernate Validator documentation of earlier versions. These points differ from what Struts 7.4.0 does:
-
The official example action imports
com.opensymphony.xwork2.ActionSupport(package renamed toorg.apache.struts2.ActionSupportin 7.0),javax.validation.constraints.*(nowjakarta.validation.constraints), andorg.hibernate.validator.constraints.EmailandNotBlank, which Hibernate Validator 9.1.4 no longer has (jakarta.validation.constraints.EmailandNotBlankreplace them;org.hibernate.validator.constraints.URLandScriptAssertare still present). The example’s@ScriptAssert(lang = "javascript", ...)also needs a JSR 223 script engine, which the JDK has not shipped since Java 15. The example class is titledBeanValidationActionand namedBeanValidationExampleActionin the code. -
The official page says that the plugin integrates with "AJAX Validation". The 7.4.0 stack has no AJAX validation interceptor of its own; client-side and AJAX validation features of the xhtml themes are deprecated in 7.4 (WW-5694), and the plugin does nothing for them.
-
The official second sample (own stack) is not valid XML:
name="beanValidation"andclass="..."are written as the text of theinterceptorelement, and it putsbeanValidationbeforedefaultStack, so beforeparams. Both are shown above in corrected form. -
The plugin page presents the WW-5757 message-lookup behaviour of 7.5.0 and 6.13.0 next to the 7.4.0 notes; the released behaviour of 7.4.0 is the earlier one described in this page.
-
Books from the Struts 2.0 and 2.1 era describe validation only as the
validationinterceptor with XML validators, theOValplugin (retired: not published at 7.4.0) as the annotation-style alternative, andcom.opensymphony.xwork2package names. In 7.4.0 the Bean Validation plugin is the annotation-style alternative; the validators of the classic framework are explained in Validation. -
struts2-bommanagesjakarta.validation-api3.0.2 although Hibernate Validator 9.x targets Jakarta Validation 3.1; the scenario worked with the managed version, but declare 3.1.1 when 3.1 features are used.
When to Choose the Plugin
Bean Validation fits when the same constraints must be shared with other layers: the entity classes of the persistence layer,
a REST controller, a service method, all validated with the same annotations (the Hibernate section of this
documentation covers the provider from the persistence side, see
Validation with Hibernate Validator). The classic framework
fits when rules depend on the request (context-dependent validators, validateXxx() per method, the visitor validator,
client-side validation of the themes, validation per alias). The two can be combined in one stack, with the costs above.
Neither choice changes the view: the s:fielderror, s:actionerror and field tags that show the errors work the same,
and the input result of the action configuration is where the user lands (see
Actions for ActionSupport and the input result, and
Interceptors for the order of the stack and
excludeMethods).
References
-
Apache Struts — Bean Validation Plugin, Bean Validation Example
-
Apache Struts 7.4.0 — Struts 2 Bean Validation Plugin (Maven site)
-
Apache Struts — plugins/bean-validation (sources, struts-plugin.xml)
-
Apache Struts — Validation (Using Annotations, Bean Validation)
-
Apache JIRA — WW-5718 (security interceptors in the bean validation stack)