Client Side Validation
Basics
Since Struts 7.4.0, the html5 theme can derive HTML5 constraint-validation
attributes (required, minlength, maxlength, pattern, min, max) directly from a field’s
server-side validators, so the browser rejects obviously-invalid input before the form is
even submitted. This replaces the older, generated-JavaScript validator used by the xhtml theme and
css_xhtml theme, which is now deprecated — see Pure JavaScript Client Side Validation
(deprecated) below.
There is also AJAX Client Side Validation, which runs the full server-side
validation stack (including visitor validators and validate()) over AJAX and is unaffected by any of this.
HTML5 Constraint Validation
Enabling it
HTML5 constraint validation is off by default. Turn it on with the struts.ui.html5.constraints constant:
struts.ui.html5.constraints=true
It only has an effect on fields rendered with the html5 theme (see Using the HTML5
theme). There is no per-form opt-in attribute —
unlike the deprecated JavaScript validator, this feature does not use <s:form validate="true">. Once the
constant is on and a field’s theme is html5, its validators are consulted automatically.
The constant defaults to
falseso existinghtml5-theme forms keep rendering unchanged. The default is expected to flip totruein a future major release, tracked by WW-5696.
The governing rule: never false-reject
The mapping is deliberately conservative. A constraint is emitted only when the browser cannot reject input the server would have accepted. If the browser rejected something the server allows, the user would be stuck with a form that will not submit and no explanation why. Being conservative simply costs a field its client-side check — that’s a harmless, quiet failure mode, so the mapping always chooses it over the alternative.
The clearest consequence of this rule: Struts never sets or changes an input’s type. A field stays
whatever type the developer gave it. In particular:
- Switching a field to
type="number"would reject a value like1234,50, which the framework’s locale-aware numeric conversion happily accepts in a comma-decimal locale. - The browsers’
emailandurlinput grammars don’t matchEmailValidatorandUrlValidator.
So min/max range constraints are only ever added to a control the developer already made numeric
(type="number" or type="range") — Struts will never promote a plain text field into one just because
an int or double validator is attached to it.
Mapping table
| Validator | Emits | Condition |
|---|---|---|
requiredstring |
required |
on text-entry controls (text, search, tel, password, email, url) and textarea |
required |
required |
only on radio and file, and only while the bound property holds a value the validator would reject (null, an empty array or an empty collection) |
stringlength |
minlength / maxlength |
on text-entry or textarea, and only if the validator has trim="false"; each attribute is added only if actually configured |
regex |
pattern |
on text-entry controls only, and only if caseSensitive="true", trim="false", the regex is ECMAScript-safe (see below), and the validator is not an email or creditcard validator (both extend RegexFieldValidator but carry grammars the browser does not share) |
int, short, long |
min / max |
only when the control is already type="number" or type="range" |
double |
min / max |
same as above; only inclusive bounds are emitted — exclusive bounds have no HTML equivalent and are omitted |
date |
— | nothing yet; temporal min/max is deferred to a future release |
email, url, creditcard |
— | never emitted |
fieldexpression, expression, conversion |
— | never emitted |
visitor |
— | nothing for the visitor itself; the visited object’s own validators apply to its nested fields (user.name) exactly as if they were declared on the action, and their messages resolve as during validation — the visited class’s bundle first, then the action’s, with ${...} read from the visited object when it exists |
| any validator carrying a message | data-msg-<validatorType> |
always added on a control that submits a value, including for validators that emit no constraint attribute at all; never on <s:label> or a control of an unknown type |
Two of these conditions are easy to miss and sharply limit how often required, minlength/maxlength,
and pattern actually show up:
required is split across two validators, and they don’t behave alike. requiredstring fails on
null, empty, and (by default) blank values, so it is strictly stricter than the browser’s required — safe
to emit on any text-entry control. Plain required, however, only fails on a null value, an empty array,
or an empty collection. That means an empty text input (which submits "", not nothing), a select with
an empty-valued option, and an unticked checkbox (CheckboxInterceptor substitutes the parameter
"false" for it) all pass server-side validation while a browser required attribute would block them.
Only radio and file controls omit their parameter entirely when left empty, so those are the only two
control types where plain required agrees with the server — which is why the table above emits required
for the required validator on those two types alone.
Even there, one more check is needed. An omitted parameter leaves the property at whatever it already
holds, and that is the value the page is rendering. A private int priority bound to a radio list of
1, 2, 3 renders with nothing checked (0 is not in the list), yet the server sees a non-null Integer and
accepts the empty submit; a file property that prepare() loaded from an existing entity — the ordinary
edit flow — is accepted the same way. So required is emitted only while the bound value is one the
validator itself would reject: null, an empty array or an empty collection. A property that is null when the
page renders but populated only on the submit request is the one case this cannot see.
Both minlength/maxlength and pattern need trim="false", which is not the default. Both
StringLengthFieldValidator.trim and RegexFieldValidator.trim default to true, so the server measures
or matches the field’s trimmed value while the HTML attribute constrains the raw one. A stringlength
validator with maxLength="4" accepts "abcd " — it trims to four characters, which is within the limit —
but a browser enforcing maxlength="4" would stop the user typing the fifth character at all. Likewise, a
regex of [a-z]+ accepts "abc " server-side (it trims to "abc" first) while the browser, matching the
raw value, blocks it. Because of this, minlength/maxlength and pattern are only ever emitted
for validators explicitly configured with trim="false" — which most existing stringlength and regex
validators are not. In practice, expect both to show up rarely until applications start setting
trim="false" deliberately for fields where it’s safe.
ECMAScript-safe means the regex uses only constructs that mean the same thing in Java’s regex engine
and in the browser’s: literals, \d/\w and their negations, character classes without POSIX or Unicode
property syntax, grouping, alternation, anchors, and bounded quantifiers. Browsers compile pattern with
the v (unicode sets) flag, which is stricter than Java inside a character class: ( ) { } / | must be
escaped there, a hyphen is accepted only as a range operator between two plain literals ([a-z]) or
escaped ([\w\-]), doubled punctuators such as .. or !! are reserved, and a class starting with a
literal ] ([]a]) is rejected. Outside a class, \- is not a legal escape and a lone ] or } is
an error, both of which Java reads as literals. A common email-shaped regex like [a-z0-9._%+-]+@ is
therefore not safe (the trailing unescaped -); [a-z0-9._%+\-]+@ is. Notably,
\s and \S are excluded — Java’s \s is ASCII-only by default while ECMAScript’s \s covers the wider Unicode
whitespace set, so a pattern like ^\S+$ would accept a value containing a non-breaking space server-side
and reject it in the browser. Any regex using a construct outside this allowlist simply gets no pattern
attribute at all — it is never rejected loudly, it just quietly doesn’t get a client-side check.
A pattern may carry a whitespace-only alternative. RegexFieldValidator skips any value that trims to
the empty string before it consults its own trim param — the check is value.trim().isEmpty() — so even
with trim="false" a single space passes the server. The browser, however, skips pattern only for the
empty string and would block that space. Unless the field also carries a requiredstring validator that
trims (its default), which rejects blank input server-side, the emitted pattern is therefore
(?:<regex>)|[\x00-\x20]*: the original regex, or a value made only of the characters String.trim()
strips. With a trimming requiredstring present, the bare regex is emitted.
data-msg-* attributes
Every validator carrying a message — even one that emits no HTML constraint attribute at all — adds a
data-msg-<validatorType> attribute (for example data-msg-email, data-msg-regex) holding the
validator’s fully resolved, internationalized message. Struts ships no JavaScript that reads these.
They exist purely as a hook: an application can write its own script to read data-msg-* and show
whichever messages it wants, in whatever way it wants, including for validators (like email or
creditcard) that never get a native browser check.
The hook is only rendered on controls that submit a value. <s:label> never does, and neither does a text
field whose type the framework does not recognise, so those carry no data-msg-* at all. The validator
type also becomes part of the attribute name, which HTML escaping does not protect; a custom validator whose
type is not a plain attribute name (letters, digits, _ and -) gets no message attribute.
requiredLabel is unrelated to the required attribute
This is a common point of confusion: the requiredLabel tag attribute only controls whether a visual
marker (usually *) is drawn next to a field’s label. It has no connection to the HTML required
attribute described above, and setting requiredLabel="true" does not make a field required in the
browser — the two are decided completely independently.
Extension point: HtmlConstraintProvider
The mapping above is implemented by StrutsHtmlConstraintProvider, the default implementation of the
HtmlConstraintProvider interface, registered under the struts.htmlConstraintProvider constant:
struts.htmlConstraintProvider=struts
An application that wants a less conservative mapping — for example, emitting pattern for
case-insensitive regexes by rewriting them, or honouring min/max on a plain text field — can register
its own HtmlConstraintProvider implementation under this constant instead of the default. This is the
escape hatch for every limitation described above: the framework’s own mapping stays deliberately
conservative, but nothing stops an application from replacing it with one that fits its own validators and
locales.
A provider receives the field’s validators, the kind of control being rendered, the object the validators
run against (the action, or the visited object under a visitor validator) and the field’s current value
as the tag resolved it — the last is what lets the default implementation judge required against the bound
property.
Two things a provider cannot do. It cannot change an input’s type — the templates have already written
it by the time the constraint map renders, so a type entry is discarded; treating an email validator
as type="email" needs a template override instead. And it cannot override an attribute the developer set
on the tag: a derived entry whose name matches a tag attribute or a dynamic attribute (compared
case-insensitively, as HTML does) is dropped, so the developer’s own value always wins.
Pure JavaScript Client Side Validation (deprecated)
Deprecated since Struts 7.4.0 (WW-5694), removed in Struts 8.0.0 (WW-5696). New applications should use the html5 theme’s constraint validation described above instead.
The <s:form validate="true"> attribute enables an older client-side validation mechanism, used by the
xhtml theme and css_xhtml theme. It uses 100% client-side JavaScript, generated from the same
validation configuration used server-side, to try to reject bad input before the form is submitted:
<s:form name="test" action="javascriptValidation" validate="true">
...
</s:form>
If a name for the form is not given, the action mapping name is used as the form name. Otherwise, a
correct action and namespace attribute must be provided to the <s:form> tag — client-side validation
requires the action name and namespace to be resolvable separately, so a form whose action is given as a
full URI (for example <s:form action="/user/submitProfile.action" validate="true">) will not get
client-side validation, even though the form still works.
Because the validation logic is repeated in generated JavaScript, only a subset of validators is
supported (required, requiredstring, stringlength, regex, email, url, int, double), it is
not available for visitor validators at all, and — being a separate implementation of each validator’s
logic — some values the JavaScript accepts may still be rejected server-side, or vice versa. This is one of
the reasons it is being replaced: the html5 theme’s constraint validation above is derived directly from
the real validators, rather than reimplementing them in JavaScript.
Example
See Client Validation example for a complete, though now-deprecated, example of the JavaScript-based client-side validation described above.