Fork me on GitHub
Edit on GitHub << back to Validation

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 false so existing html5-theme forms keep rendering unchanged. The default is expected to flip to true in 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:

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.

Follow @x