Bean Validation in Jakarta Faces
|
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. |
Jakarta Validation (still widely called Bean Validation) lets a model class declare its own rules as annotations, such as
@NotBlank, @Size(max = 40) or @Min(1), so that the same rules protect every layer that touches the object: the
web form, the REST endpoint, the service method and the database mapping. Jakarta Faces integrates with it directly. An
input component whose value expression points at a constrained property is validated against those annotations during
the Process Validations phase, with no validator attached to the page, and a violation becomes an ordinary
FacesMessage shown by h:message.
This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Jakarta
Validation 3.1 with Hibernate Validator 9.1.4.Final as the provider, Eclipse Mojarra 4.1.17 and Apache MyFaces Core
4.1.4, with Java 17 or later. Faces 4.0 (Jakarta EE 10, Bean Validation 3.0) behaves the same for everything below; the
version differences are collected in What Changed. Every example was compiled and run in a scratch Bookshelf WAR served
by Tomcat 11.0.26 with Weld 6.0.4.Final, requested over HTTP, and the messages and outcomes quoted below are what came
back (checked on 2026-10-08). The same scenarios were repeated on MyFaces 4.1.4 and are compared in Mojarra and MyFaces. The
constraint tests were run with JUnit Jupiter 5.13.4.
Why Use Bean Validation in a Faces Application
Faces has had its own validation model since 1.0: required="true", the standard validator tags f:validateLength,
f:validateLongRange, f:validateRegex and friends, custom Validator classes, and validator methods. Those rules
live on the page. That works for a single form and degrades as the application grows, because the same rule ("a rating
is a whole number from 1 to 5") is then repeated in every view that edits a review, again in the REST resource that
accepts one, and again in the persistence layer. The specification states the goal plainly: Bean Validation avoids that
duplication by defining metadata that can be shared by any layer of the application, and Faces delegates field-level
validation to it so that the page author does not have to write a validator per input.
There are three parts to the integration, and the rest of this page takes them in turn:
-
Field-level validation. The standard validator
jakarta.faces.Bean(classjakarta.faces.validator.BeanValidator) looks up the property behind an input’svalueexpression and validates just that property with the value the user submitted. It is installed automatically on every input (Dependencies and Activation), andf:validateBeanconfigures it on purpose (Validation Groups and f:validateBean, Turning Bean Validation Off Where It Does Not Belong). -
Class-level validation. Rules that involve several fields at once, such as "the return date is after the pick-up date", cannot be checked one field at a time.
f:validateWholeBeanvalidates a temporary copy of the bean once its individual fields have passed (Class-Level Validation with f:validateWholeBean). -
Messages. The text of each violation is interpolated by the Validation provider, localized with the view’s locale, and wrapped in a
FacesMessage(Messages: From Violation to FacesMessage).
The two systems are complementary rather than competing. The standard Faces validators remain the right tool for rules
that belong to one page only, for conversion-adjacent checks, and for required with a page-specific message. The
validators page of this section covers them, the
converters page covers the step that runs before validation, and the
core tags page lists f:validateBean and f:validateWholeBean beside the other f: tags.
Dependencies and Activation
Faces does not ship Bean Validation. It looks for a Jakarta Validation provider on the class path and, if there is none, the integration is simply off. The specification’s activation rule has three parts:
-
If a Validation provider is present at startup, the runtime adds the validator id
jakarta.faces.Beanto the application’s default validator ids (Application.addDefaultValidatorId()), which puts aBeanValidatoron everyEditableValueHolderin every view without any markup. -
If
jakarta.faces.VALIDATE_EMPTY_FIELDSis not set tofalse, empty and null submitted values are also validated, so that@NotNulland@NotEmptycan reject them (Empty Values: Two Context Parameters That Decide Whether Constraints See Them). -
When the
BeanValidatorruns, it obtains aValidatorFactoryfrom the servlet context attribute named byBeanValidator.VALIDATOR_FACTORY_KEY(the stringjakarta.faces.validator.beanValidator.ValidatorFactory) if one is stored there, and otherwise fromValidation.buildDefaultValidatorFactory(). If no provider can be found and the validator is used, aFacesExceptionis raised (Providing Your Own ValidatorFactory).
On a Jakarta EE server (WildFly, GlassFish, Payara, Open Liberty, TomEE; none of them was run for this page, the statement follows the platform specification) the container supplies the Validation API, a
provider, an Expression Language implementation and the ValidatorFactory attribute, so the application declares
nothing. On Tomcat, or any plain servlet container, the application brings the provider. The Maven coordinates used for
the scratch Bookshelf (a WAR whose Faces implementation, Weld and the provider are all packaged in WEB-INF/lib):
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>11.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.faces</artifactId>
<version>4.1.17</version>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-shaded</artifactId>
<version>6.0.4.Final</version>
</dependency>
<!-- the Jakarta Validation provider; pulls in jakarta.validation-api 3.1.1 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.1.4.Final</version>
</dependency>
<!-- only for CDI injection into constraint validators, see "Injecting CDI Beans" -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<version>9.1.4.Final</version>
</dependency>
</dependencies>
Source: Hibernate Validator 9.1 reference guide, Getting started and Jakarta Faces 4.1 specification, 3.5.6 Bean Validation Integration
Hibernate Validator needs an implementation of Jakarta Expression Language to evaluate expressions in violation
messages. A server provides one, and Tomcat includes its own, so nothing is added in the example above. In a unit test
that runs outside any container the test needs one (org.glassfish.expressly:expressly:6.0.0 in the test scope, the
artifact the Hibernate Validator guide names), and for environments that cannot provide one at all the guide describes a
non-compliant ParameterMessageInterpolator that handles only {...} parameters.
Two context parameters control the automatic installation:
| Parameter | Default | Effect |
|---|---|---|
|
|
When |
|
|
Enables |
A <default-validators> element in faces-config.xml can also list validator ids, and a configuration resource that
declares the list overrides one from a previously processed resource. The jakarta.faces.Bean id is guaranteed to be in
the result of Application.getDefaultValidatorInfo() regardless of that configuration unless
DISABLE_DEFAULT_BEAN_VALIDATOR is true.
Source: Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters and 3.5 Validation Model
With DISABLE_DEFAULT_BEAN_VALIDATOR set to true, an input bound to a constrained property was no longer validated,
while the same input with <f:validateBean/> nested inside it was. This is the escape hatch for an application that
wants Bean Validation only on a few forms.
Constraining the Model
Constraints are annotations on a field, a getter or a type. The built-in ones live in the package
jakarta.validation.constraints:
| Constraint | Meaning |
|---|---|
|
The value must (not) be |
|
Not |
|
A |
|
Length of a string, size of a collection, map or array. |
|
Numeric bounds; the decimal variants take a string. |
|
Sign of a number. |
|
Maximum number of integral and fractional digits. |
|
A date or time relative to the current moment, including the |
|
The value matches a regular expression. |
|
A syntactically valid e-mail address. |
|
A boolean property is true or false. |
Every built-in constraint takes message, groups and payload, and every one is repeatable. Almost all of them treat
null as valid, so that "the value is optional" and "the value, if present, has this shape" can be expressed
separately. Presence is the job of @NotNull, @NotEmpty or @NotBlank, which is the single most important thing to
remember when reading the rest of this page.
The Bookshelf review form edits a Review. It is an ordinary class, free of any Faces API:
package org.example.bookshelf.model;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDate;
import org.example.bookshelf.validation.Isbn;
import org.example.bookshelf.validation.Publish;
public class Review implements Serializable {
@NotBlank
@Size(max = 40)
private String reviewer;
@NotNull
@Min(1)
@Max(5)
private Integer rating;
@NotNull
@Size(min = 3)
private String title;
@NotBlank
private String author;
@Isbn
private String isbn;
@Size(max = 500)
@NotBlank(groups = Publish.class, message = "A published review needs a comment")
private String comment;
@PastOrPresent
private LocalDate readOn;
// getters and setters omitted
}
@Isbn and Publish are defined in Custom Constraints and Messages and Validation Groups and f:validateBean. The view binds each input to a property of
that object. Note that nothing in the XHTML mentions Bean Validation:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head><title>Review</title></h:head>
<h:body>
<h:form id="f">
<h:messages globalOnly="true"/>
<h:outputLabel for="reviewer" value="Reviewer"/>
<h:inputText id="reviewer" label="Reviewer" value="#{reviewBean.review.reviewer}"/>
<h:message for="reviewer"/>
<h:outputLabel for="rating" value="Rating"/>
<h:inputText id="rating" label="Rating" value="#{reviewBean.review.rating}"/>
<h:message for="rating"/>
<h:inputText id="title" label="Title" value="#{reviewBean.review.title}"/>
<h:message for="title"/>
<h:inputText id="isbn" label="ISBN" value="#{reviewBean.review.isbn}"/>
<h:message for="isbn"/>
<h:inputText id="readOn" label="Read on" value="#{reviewBean.review.readOn}">
<f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/>
</h:inputText>
<h:message for="readOn"/>
<h:commandButton value="Save" action="#{reviewBean.save}"/>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.example.bookshelf.model.Review;
@Named
@ViewScoped
public class ReviewBean implements Serializable {
private final Review review = new Review();
@Inject
private FacesContext facesContext;
public Review getReview() {
return review;
}
public void save() {
// reached only when every input in the form passed conversion and validation
facesContext.addMessage(null, new FacesMessage("Saved " + review.getReviewer()));
}
}
Source: Jakarta EE Tutorial: Using the Standard Validators and Jakarta Faces 4.1 specification, 3.5.6 Bean Validation Integration
Submitting the form with an empty reviewer, a rating of 7, an ISBN of 123 and a future date produced, next to the
respective inputs:
| Input | Message (English locale) |
|---|---|
reviewer ( |
must not be blank |
rating ( |
must be less than or equal to 5 |
isbn ( |
Not a valid ISBN-13 (got '123') |
readOn ( |
must be a date in the past or in the present |
The save() action did not run and the bean was not changed. That is the behavior of every validator in Faces and the
subject of the next section.
How Faces Finds the Constraints
The validator does not look at the bean as a whole. For one input it asks the component’s value expression for its
ValueReference, which names a base object and a property: for #{reviewBean.review.rating} the base is the
Review instance and the property is rating. It then asks the Validation provider to check just that property
against the submitted value (Validator.validateValue(baseClass, property, value, groups)), so only the constraints on
that field or getter, in the selected groups, are evaluated. The documented consequences, each confirmed by running it:
-
A getter, a field, a nested object, a repeated row, a backing bean. Any of these works as long as the base is an object whose class has constraints. A
#{r.rating}insideui:repeat var="r"was validated per row, and the message appeared next to the row that failed. A constrained property declared directly on a@ViewScopedbacking bean, bound as#{reviewBean.nickname}, was validated too, even though EL reaches the bean through a CDI client proxy. -
A map entry or list element.
#{reviewBean.extra['k']}has the map as its base. AMaphas no constraints onk, so nothing was checked and the input silently accepted any value. The same applies to a literal value expression or one without a base object. -
A class without any constraints. If the provider reports no constraints for the base class, the validator returns without doing anything. A form with hundreds of inputs over unconstrained beans costs only the descriptor lookup.
-
Nothing cascades. Only the one property is validated. The Validation specification states that
@Validis not honored byvalidateValue(), so a@Validon a nested object is a rule forValidator.validate(object)and plays no part when Faces validates a single input; each nested property that is bound to an input is validated through its own value expression. The same specification saysvalidateValue()finds the property on the given class or any of its superclasses, which is why a CDI client proxy, a subclass of the bean, still sees the constraints of the bean class.
Where Validation Happens in the Lifecycle
Bean Validation is a Faces validator, so it runs where every validator runs: in the Process Validations phase, on each input component of the submitted components in the order they appear in the tree. The sequence for one input is shown below, and the lifecycle page of this section places it among the six phases (see Process Validations there).
Four observations follow from the diagram, and each was confirmed on Mojarra and MyFaces:
-
A conversion failure hides the constraints. A
readOnofbadproduced only the converter’s message (Read on: 'bad' could not be understood as a date. Example: 2026-10-08). The converter’s job comes first, and a value that cannot be converted has nothing to validate. -
required="true"wins over everything. An emptyauthorbound to a@NotBlankproperty and markedrequired="true"produced onlyAuthor: Validation Error: Value is required., and the Bean Validation message never appeared. A required input is rejected before the validators are consulted. Declare presence once, either withrequiredand a page-specificrequiredMessage, or with@NotNullor@NotBlankon the model. Mixing both is harmless but gives the user the page’s wording instead of the model’s. -
One invalid input does not stop the others from being validated. Each input is processed and each one with a violation gets its own message. When any message has been queued, the lifecycle skips Update Model Values and Invoke Application and goes to Render Response. The model objects keep their previous values, which is the property that makes "validate, then apply" safe.
-
The action method is not called. That is why
save()above did not run. ACancelbutton that must work with invalid fields usesimmediate="true", with the consequences described in the immediate section of the lifecycle page.
Empty Values: Two Context Parameters That Decide Whether Constraints See Them
A text field that the user leaves empty is submitted as the empty string. Two context parameters decide what the
validation machinery does with it, and getting them wrong is the commonest reason a @NotNull seems to be ignored.
jakarta.faces.VALIDATE_EMPTY_FIELDS-
Controls whether validators run at all when the submitted value is empty or
null. The values aretrue,falseandauto, which is the default when the parameter is missing:autobehaves astruewhen a Bean Validation provider is present and asfalseotherwise. Older Faces releases skipped validators for an empty value (stated in the project notes for this page, not re-checked against the 1.x specification). Since Bean Validation is part of Faces, validators must run on empty values, otherwise a@NotNullcould never reject anything, as a validator that does not run cannot fail. jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL-
When
true,UIInput.validate()turns a zero-length submitted string intonullbefore converting and validating. The default isfalse, so an empty text field reaches the model as"", notnull. It is a global switch, not an attribute of a single input.
The two interact with the constraints in a way that surprises people. The table shows what a blank title produced
with @NotNull @Size(min = 3) on the property, on Mojarra 4.1.17 (the same on MyFaces 4.1.4):
| Configuration | Outcome for an empty title |
|---|---|
Defaults ( |
|
|
|
|
No validator runs for an empty field. The review was saved with an empty reviewer and a |
The third row is the dangerous one: false is a configuration that was needed to keep old validators from receiving
empty values, but it also switches off every @NotNull, @NotEmpty and @NotBlank in the application. Leave the
parameter alone unless an existing validator cannot cope.
The validators page shows the same two parameters with the standard validator tags. The practical rule is to pick one convention and write constraints that match it:
-
Set
INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULLtotrueinweb.xml, so that "nothing typed" meansnulleverywhere (the Jakarta EE Tutorial recommends exactly this for Bean Validation to work as intended). Then@NotNullmeans "required", and optional text fields with@Size(min = 3)or@Patternaccept an empty input without a complaint. -
Use
@NotBlankfor required text. It rejects bothnulland""and, unlike@NotNull, does not depend on the parameter. This is what theReviewabove does forreviewerandauthor. -
Do not combine
@NotNull @Size(min = 3)on a text field and expect the user to see the@NotNullmessage when the field is empty, unless the parameter istrue.
<context-param>
<param-name>jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
<param-value>true</param-value>
</context-param>
Source: Jakarta EE Tutorial: Validating Null and Empty Strings and Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
Numeric and date inputs behave differently from text. An empty Integer field is converted to null by the standard
converter, so @NotNull on rating rejected it (must not be null) under the defaults. The parameter matters for
String properties.
Validation Groups and f:validateBean
A validation group is an interface that tags constraints so that a validation call can select a subset of them. A
constraint without groups belongs to the built-in group jakarta.validation.groups.Default. Groups answer questions
the single-set model cannot, such as "a draft may be saved with an empty comment, but a published review needs one":
package org.example.bookshelf.validation;
/** Validation group: constraints enforced only when a review is published. */
public interface Publish {
}
Review.comment above carries @NotBlank(groups = Publish.class, ...). By default Faces validates the Default group
only, so this constraint is dormant. f:validateBean chooses the groups for the inputs it applies to. Its attributes
come from the VDL:
| Attribute | Type | Meaning |
|---|---|---|
|
String (EL allowed) |
A comma-separated list of validation groups, each the fully qualified class name of the group interface. Blank, or
only commas and non-word characters (the documented pattern |
|
Boolean (EL allowed) |
Whether this validator is turned off for the enclosing component or branch (Turning Bean Validation Off Where It Does Not Belong). |
|
String |
Used inside a composite component: names an attached object that the validator is applied to instead of the component it is nested in. |
|
|
Binds the validator instance to a bean property. Rarely needed. |
The tag works in two ways. Nested in one input, it applies to that input only. Wrapped around several inputs, it sets a
branch default for every input inside it. The validation groups come from the nearest enclosing f:validateBean
that has a value, and if there is none, the Default group is used. A page that edits a review in "publish" mode
therefore wraps its inputs once instead of nesting a tag in each:
<f:validateBean validationGroups="jakarta.validation.groups.Default,org.example.bookshelf.validation.Publish">
<h:inputText id="reviewer" label="Reviewer" value="#{reviewBean.review.reviewer}"/>
<h:message for="reviewer"/>
<h:inputTextarea id="comment" label="Comment" value="#{reviewBean.review.comment}"/>
<h:message for="comment"/>
</f:validateBean>
The observed behaviors of this branch, with reviewer empty, rating 9 and comment empty:
| validationGroups | Result |
|---|---|
none (the |
|
|
Only |
|
All three messages. |
|
The request failed with a |
a blank or comma-only value |
Treated as no value: the |
Two consequences of the table. The group list replaces the default, it does not add to it, so a branch that lists
only Publish stops validating the Default constraints; add jakarta.validation.groups.Default explicitly when you
want both. And a group name is a class name, so a typo is an exception at the first submit, not a silently ignored
rule. A @GroupSequence on a group interface, described in the Validation specification, orders groups so that
expensive checks run only after cheap ones pass, and is selected through the same attribute.
Choosing the Group by Button
A common requirement is one form with a Save draft button and a Publish button, validated differently. The
validation groups are evaluated while the view is processed, before any action runs, so a bean property set by the
action cannot choose them. What the request does carry is the name of the button that was pressed. Evaluating the EL in
validationGroups against the request parameters worked in a scratch run:
<h:form id="f">
<h:messages globalOnly="true"/>
<f:validateBean validationGroups="#{empty param['f:publish'] ? 'jakarta.validation.groups.Default' : 'jakarta.validation.groups.Default,org.example.bookshelf.validation.Publish'}">
<h:inputText id="reviewer" label="Reviewer" value="#{reviewBean.review.reviewer}"/>
<h:message for="reviewer"/>
<h:inputTextarea id="comment" label="Comment" value="#{reviewBean.review.comment}"/>
<h:message for="comment"/>
</f:validateBean>
<h:commandButton id="save" value="Save draft" action="#{reviewBean.save}"/>
<h:commandButton id="publish" value="Publish" action="#{reviewBean.save}"/>
</h:form>
Pressing Save draft with an empty comment saved the review. Pressing Publish gave Comment: A published review
needs a comment. The expression is re-evaluated on every request, so it picks up the button of the current postback
even though the view itself was restored from an earlier request. The coupling is to the client id (f:publish), so
this is a technique to keep in one place. When the two actions are really two screens, two forms each with its own
f:validateBean are easier to read.
Turning Bean Validation Off Where It Does Not Belong
The disabled attribute switches the validator off for the input or the branch it applies to. It takes a boolean or an
EL expression. Two cases are worth knowing.
-
One input inside an active branch.
<f:validateBean disabled="true"/>nested in an input wins over the branch settings for that input. A field whose value is checked elsewhere (a search box, a password field that is validated by the identity provider) opts out like this. Run on Mojarra 4.1.17 (Tomcat 11.0.26, Hibernate Validator 9.1.4): a rating of9against@Max(5)posted with the tag nested in the input, once on its own and once inside an enclosingf:validateBean validationGroups="jakarta.validation.groups.Default", produced no message for the rating while the emptyreviewernext to it still failed. MyFaces was not run for this check. -
A whole branch.
<f:validateBean disabled="#{...}">around a group of inputs disables Bean Validation for all of them. Withdisabledtrue on the branch, a rating of9was accepted and saved in the scratch run.
<h:inputText id="isbn" label="ISBN" value="#{reviewBean.review.isbn}">
<f:validateBean disabled="true"/>
</h:inputText>
A form that edits only part of an object needs no disabled at all. Constraints are evaluated property by property for
the inputs that are actually in the submitted form, never on the whole object, so a "quick rating" widget that binds
only rating is not blocked by a missing title. This is a difference from calling Validator.validate(review)
yourself, which checks every property.
Class-Level Validation with f:validateWholeBean
Field-level validation cannot express a rule over several properties. The specification explains why: Faces conversion
and validation finish for every input before any value is pushed into the model, so at validation time the bean still
holds the old values, and a constraint on the class would see yesterday’s data. Faces 2.3
added f:validateWholeBean for this case. It is activated by jakarta.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN, which
is false by default; with the parameter unset the tag is a no-op. The scratch Bookshelf confirms it: an invalid period
was accepted when the parameter was missing and rejected when it was true.
The mechanism, from the VDL:
A class-level constraint is an annotation on the type plus a ConstraintValidator for it. The Bookshelf borrow form
asks for a pick-up and a return date and requires the second to come after the first:
package org.example.bookshelf.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = ValidPeriodValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface ValidPeriod {
String message() default "The return date must be after the pick-up date";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package org.example.bookshelf.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.example.bookshelf.model.BorrowRequest;
public class ValidPeriodValidator implements ConstraintValidator<ValidPeriod, BorrowRequest> {
@Override
public boolean isValid(BorrowRequest request, ConstraintValidatorContext context) {
if (request == null || request.getFrom() == null || request.getTo() == null) {
return true; // a missing value is reported by the @NotNull constraints on the fields
}
return request.getFrom().isBefore(request.getTo());
}
}
package org.example.bookshelf.model;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.LocalDate;
import org.example.bookshelf.validation.PeriodChecks;
import org.example.bookshelf.validation.ValidPeriod;
@ValidPeriod(groups = PeriodChecks.class)
public class BorrowRequest implements Serializable {
@NotNull(groups = PeriodChecks.class)
private LocalDate from;
@NotNull(groups = PeriodChecks.class)
private LocalDate to;
// getters and setters omitted
}
PeriodChecks is an empty marker interface like Publish. Notice that every constraint carries the group, and that
isValid tests for null and treats a missing value as valid, because a missing value is the job of the @NotNull
constraints. When f:validateWholeBean calls the validator, the specification guarantees that the individual properties
have already passed, so the guard is never needed there; but other callers give no such guarantee. Anyone who calls
Validator.validate(request) in the PeriodChecks group directly gets the validator called with whatever the object
holds, and the same goes for the class-level check of OmniFaces o:validateBean (see the
OmniFaces page): an earlier version of this listing without the guard failed with an
HTTP 500 under o:validateBean in a scratch run made while writing that page (not repeated here). The guarded listing above
was compiled with Hibernate Validator 9.1.4 and run directly: an empty request produced only the two @NotNull
violations, a return date before the pick-up date produced The return date must be after the pick-up date, and a valid
period produced none.
The view uses the group on each contributing input and on the tag. The VDL states that the groups attribute is required
on f:validateWholeBean and that the feature requires it on the f:validateBean tags too:
<h:form id="f">
<h:messages globalOnly="true"/>
<h:inputText id="from" label="From" value="#{borrowBean.request.from}">
<f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/>
<f:validateBean validationGroups="org.example.bookshelf.validation.PeriodChecks"/>
</h:inputText>
<h:message for="from"/>
<h:inputText id="to" label="To" value="#{borrowBean.request.to}">
<f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/>
<f:validateBean validationGroups="org.example.bookshelf.validation.PeriodChecks"/>
</h:inputText>
<h:message for="to"/>
<f:validateWholeBean id="period" value="#{borrowBean.request}"
validationGroups="org.example.bookshelf.validation.PeriodChecks"/>
<h:message for="period"/>
<h:commandButton value="Request" action="#{borrowBean.submit}"/>
</h:form>
Source: Jakarta Faces 4.1 VDL, f:validateWholeBean and Jakarta Faces 4.1 specification, 3.5.6.3 Class-Level Validation
With a pick-up of 2026-10-10 and a return of 2026-10-09 the request produced The return date must be after the
pick-up date, attached to the period component. With the pick-up empty the result was From: must not be null only:
the class-level check was skipped because a contributing field was invalid, as specified. With 2026-10-01 and
2026-10-09 the action ran.
The rules of the tag, each one a way the feature can fail:
-
Enable the parameter. Otherwise the tag does nothing, with no log line to say so.
-
Place it after all the inputs it covers, in the same form. The specification says the result is unspecified otherwise. Mojarra enforces it: the same view with
f:validateWholeBeanbefore the inputs failed to render with anIllegalArgumentException,f:validateWholeBean must be placed at the end of UIForm, and MyFaces also failed with an HTTP 500 in that case. -
The bean must be copyable. The VDL lists the order in which a copy is attempted:
newInstance()on the bean’s class, thenSerializable, thenCloneable, then a copy constructor, and aFacesExceptionif none yields a copy. A class that has none of those failed in the scratch run on both implementations; Mojarra’s message wasCan't copy object of type class ... No copier appeared to be capable of copying it.Make the model classSerializable, which a@ViewScopedor session-held object has to be anyway. -
The
valueis any object, not necessarily a backing bean. The specification describes "CDI based backing beans", but#{borrowBean.request}pointed at a plain nested object and worked. The constraint lives on that object’s class. -
Messages are attached to the whole-bean component, so give it an
idand a matchingh:message for. ItsFacesMessagetext is the constraint’s message; when the application overrides theBeanValidatormessage format with a label placeholder (Messages: From Violation to FacesMessage), this component has no label and the client id (f:period) appears as the prefix. -
A group is mandatory. The VDL marks
validationGroupsas required onf:validateWholeBean. Putting the class-level constraint in a dedicated group also keeps it out of theDefaultgroup, which other code paths (a service call, persistence) validate on objects whose dates may not be populated yet.
Custom Constraints and Messages
The built-in annotations cover generic shapes. A domain rule, such as "this is a valid ISBN-13", is a custom constraint:
an annotation plus a ConstraintValidator. The annotation must declare the three standard elements message,
groups and payload:
package org.example.bookshelf.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = IsbnValidator.class)
@Target({FIELD, METHOD, PARAMETER})
@Retention(RUNTIME)
public @interface Isbn {
String message() default "{org.example.bookshelf.validation.Isbn.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package org.example.bookshelf.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class IsbnValidator implements ConstraintValidator<Isbn, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // presence is the job of @NotNull or @NotBlank
}
String digits = value.replace("-", "");
if (digits.length() != 13 || !digits.chars().allMatch(Character::isDigit)) {
return false;
}
int sum = 0;
for (int i = 0; i < 13; i++) {
sum += (digits.charAt(i) - '0') * (i % 2 == 0 ? 1 : 3);
}
return sum % 10 == 0;
}
}
org.example.bookshelf.validation.Isbn.message=Not a valid ISBN-13 (got '${validatedValue}')
Source: Jakarta EE Tutorial: Bean Validation: Advanced Topics and Jakarta Validation 3.1 specification, Constraint definition
Four rules make validators well-behaved:
-
Treat
null(and, for text, the empty string) as valid. That keeps the constraint orthogonal to presence, exactly like the built-ins. The validator above returnstruefor both. -
Keep the validator stateless and fast. One instance can serve concurrent requests and is created through the provider’s
ConstraintValidatorFactory. -
Do not touch the persistence layer. The specification forbids calling an
EntityManagerthat might call back into validation from a validator, because it would flush recursively. -
Give the annotation a message key and ship the text separately. The key goes in
ValidationMessages.properties(andValidationMessages_es.properties, …) at the root of the class path, which the provider searches before its own built-in bundle. The same file can override built-in messages: withjakarta.validation.constraints.NotBlank.message=Please fill in this fieldin it, the reviewer message readPlease fill in this field.
A new constraint can also be composed from existing ones, with @Constraint(validatedBy = {}) and the building-block
annotations on it, as in the tutorial’s @USPhoneNumber, which applies a @Pattern. A composed constraint needs no
validator class.
Messages use two forms of interpolation: {key} looks up a bundle entry or an annotation attribute ({min}), and
${...} evaluates a Jakarta Expression Language expression, such as ${validatedValue} for the offending value.
Injecting CDI Beans into Constraint Validators
A validator often needs a service: "this ISBN exists in the catalogue", "this username is free". The natural code injects it:
package org.example.bookshelf.validation;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.example.bookshelf.model.Catalogue;
public class KnownIsbnValidator implements ConstraintValidator<KnownIsbn, String> {
@Inject
private Catalogue catalogue;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || value.isEmpty() || catalogue.contains(value);
}
}
@KnownIsbn is declared like @Isbn. On a Jakarta EE server it works as written, because the specification requires
a Jakarta EE container’s Validation provider to integrate with CDI, so the ValidatorFactory is CDI-aware and
validators are created through the bean manager. On Tomcat with Weld it does not: Faces asks for
Validation.buildDefaultValidatorFactory(), which knows nothing about the bean manager, and the field stays null.
The scratch run on Tomcat 11.0.26, Weld 6.0.4.Final, Mojarra 4.1.17 and Hibernate Validator 9.1.4.Final produced an
HTTP 500 with NullPointerException: Cannot invoke "...Catalogue.contains(String)" because "this.catalogue" is null.
Adding hibernate-validator-cdi to the class path by itself did not change that, because Faces still built its own
default factory. The fix that worked is to give Faces the CDI-aware factory under the documented servlet context
attribute, from a listener (Providing Your Own ValidatorFactory).
Providing Your Own ValidatorFactory
The ValidatorFactory that Faces uses is read from the servlet context attribute
BeanValidator.VALIDATOR_FACTORY_KEY. Anything stored there wins over the default bootstrap, which makes it the
extension point for a custom message interpolator, a custom ConstraintValidatorFactory, a configured clock or a
ParameterMessageInterpolator. With hibernate-validator-cdi on the class path the extension registers a CDI-aware
ValidatorFactory bean, which a @WebListener can inject (Weld on Tomcat supports injection in listeners) and publish:
package org.example.bookshelf.validation;
import jakarta.faces.validator.BeanValidator;
import jakarta.inject.Inject;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import jakarta.validation.ValidatorFactory;
@WebListener
public class ValidatorFactoryListener implements ServletContextListener {
@Inject
private ValidatorFactory validatorFactory; // CDI-aware, provided by hibernate-validator-cdi
@Override
public void contextInitialized(ServletContextEvent event) {
event.getServletContext().setAttribute(BeanValidator.VALIDATOR_FACTORY_KEY, validatorFactory);
}
}
With this listener the reservation form accepted 978-0-306-40615-7 (Reserved 978-0-306-40615-7) and rejected
978-1-4028-9462-6 with This ISBN is not in the catalogue, on Mojarra and on MyFaces. The same effect without the
extra dependency was obtained by building the factory by hand with
Validation.byDefaultProvider().configure().constraintValidatorFactory(...), where the factory creates each validator
through BeanManager.getInjectionTargetFactory(...) (produce, inject, postConstruct) and releases it with
preDestroy and dispose, which is the sequence the Validation specification prescribes. The provider owns the
factory’s life cycle, so the injected variant needs no contextDestroyed; a hand-built factory should be closed there.
Messages: From Violation to FacesMessage
Each ConstraintViolation becomes a FacesMessage whose summary and detail are both the violation’s interpolated
message, and the whole set is carried by one ValidatorException. Three things shape what the user reads.
The message format. The text goes through the Faces message jakarta.faces.validator.BeanValidator.MESSAGE
(BeanValidator.MESSAGE_ID). It has two placeholders: {0} is the interpolated Validation message and {1} is the
label of the input. The default format is just {0}, so by default the Validation provider’s wording is shown
untouched. An application that wants the label in front of every message, in line with the standard Faces messages that
say Rating: Validation Error: ..., overrides it in a message bundle registered with
<message-bundle>:
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee" version="4.1">
<application>
<message-bundle>org.example.bookshelf.FacesMessages</message-bundle>
</application>
</faces-config>
jakarta.faces.validator.BeanValidator.MESSAGE={1}: {0}
With that bundle, the messages read Reviewer: must not be blank and Rating: must be less than or equal to 5, using the
label attribute of each input. An input without a label shows its client id instead (f:rating: must be less than or
equal to 5 in the scratch run), so give every input a label when you use {1}. Faces' own messages for
conversion and required are separate keys in the same bundle. The messages and internationalization page
lists the standard keys and builds the English and Spanish bundles.
Several violations on one input. The component receives all of them. h:message renders only the first one for a
component, as its documentation says, while h:messages lists every one. An ISBN that was both malformed and unknown to
the catalogue showed This ISBN is not in the catalogue in h:message and both lines in h:messages. The order of the
violations is not specified by Validation, so do not write a page that depends on it.
Locale. The specification asks Faces to give the provider a message interpolator that resolves the Locale from the
current UIViewRoot, and the BeanValidator does so (it decorates the factory’s interpolator). The messages therefore
follow f:view locale or the browser’s negotiated locale, not the JVM default:
<f:view locale="#{param.lang == null ? 'en' : param.lang}">
<h:head><title>Review</title></h:head>
<h:body>
<h:form id="f">
<h:inputText id="rating" label="Rating" value="#{reviewBean.review.rating}"/>
<h:message for="rating"/>
</h:form>
</h:body>
</f:view>
With ?lang=es the rating message came back as Rating: debe ser menor que o igual a 5, and the custom @Isbn message
came from ValidationMessages_es.properties. One trap in the scratch run is a plain Java one: on a machine whose JVM
default locale was Spanish, a request in English found no ValidationMessages_en.properties and the resource bundle
lookup fell back to the JVM default locale (ValidationMessages_es) before the base file, so an English view showed
the Spanish @Isbn text. Java’s ResourceBundle consults the default locale before the root bundle. Ship an explicit
file for every locale you support, including the one in the base file (ValidationMessages_en.properties next to the
unsuffixed one), or pin the default locale on servers.
Escaping and sensitive values. The message may contain the rejected value through ${validatedValue}. Faces
escapes message text when h:message renders it: an ISBN of <script>alert(1)</script> came out as
<script>... in the page (output escaping in general is on the security page). The risk lies in rendering messages with something that does not escape, such as a
component or a custom renderer with escaping disabled, or in logging the message. Never echo the value of a password,
token or card-number field in a message, and never use escape="false" on a component that renders messages built from
user input.
Bean Validation with Ajax
Nothing about Bean Validation changes under f:ajax (see the Ajax page for execute and render): the Ajax request runs the same six phases on the components in its
execute list, and the validators run in Process Validations. The scope of execute decides which constraints are
evaluated. An input with <f:ajax event="blur" render="m_rating"/> has the default execute="@this", so a blur
validates that single input and renders its message, and a button with <f:ajax execute="@form" render="panel"/>
validates the whole form.
<h:form id="f">
<h:panelGroup id="panel" layout="block">
<h:messages globalOnly="true"/>
<h:inputText id="rating" label="Rating" value="#{reviewBean.review.rating}">
<f:ajax event="blur" render="m_rating"/>
</h:inputText>
<h:message id="m_rating" for="rating"/>
<h:commandButton value="Save" action="#{reviewBean.save}">
<f:ajax execute="@form" render="panel"/>
</h:commandButton>
</h:panelGroup>
</h:form>
Replaying both requests against Mojarra, the blur request returned a partial response whose update for f:m_rating was
Rating: must be less than or equal to 5, and the save request re-rendered the panel with the same message. Three
details matter in practice. The message component must be inside the render list, or the user sees nothing change. A
message for an input that is not in execute is not produced, so a blur on field A never reports a problem with field
B. And an invalid field in an Ajax submit skips the action exactly as in a full submit, which is why the Save button
above shows messages and does not call save().
Defence in Depth: The Service Layer and the Database
Validation in the web tier protects the user experience, not the data. A REST call, a batch job, a test or a future screen reaches the same model without a Faces form in front of it. The constraints on the model are the common ground, and Jakarta Validation offers two more places to enforce them.
-
Method validation on CDI beans. The specification requires that parameters and return values of constrained methods of CDI beans are validated when the provider integrates with CDI (priority
Interceptor.Priority.PLATFORM_AFTER+800). Withhibernate-validator-cdion Tomcat, a service methodregister(@NotBlank @Isbn String isbn)on an@ApplicationScopedbean threw aConstraintViolationExceptionfor123and for an empty string, which the backing bean caught and turned intoFacesMessage`s (the messages were `Not a valid ISBN-13 (got '123')and, with theNotBlankoverride shown earlier,Please fill in this field). The property path readsregister.arg0unless the code is compiled with-parameters. -
Persistence. The Validation specification leaves the integration with Jakarta Persistence to the persistence specification, which can validate entities on lifecycle events; that integration is outside this page (the persistence page shows the service and repository layers it sits in).
A Faces form that catches ConstraintViolationException from the service and shows it is the right fallback for the
cases the page-level constraints cannot see, such as a rule that depends on other rows. The rule of thumb is to keep the
constraints on the model, let Faces enforce the field-level ones through the default validator, and let the service
enforce the rest.
Testing Constraints
A constraint is plain Java and needs no container to test; the testing page covers the wider test strategy (constraints, converters, validators and the form itself). Build a Validator, validate an object and assert on the
violations. Assert on the message template rather than the interpolated text: the text depends on the default locale
of the machine running the build (a test that compared must be less than or equal to 5 failed on a Spanish-locale
workstation in the scratch run).
package org.example.bookshelf.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import java.util.Set;
import org.example.bookshelf.validation.Publish;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class ReviewConstraintsTest {
private static ValidatorFactory factory;
private static Validator validator;
@BeforeAll
static void createValidator() {
factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@AfterAll
static void closeFactory() {
factory.close();
}
private static Review validReview() {
Review review = new Review();
review.setReviewer("Ann");
review.setRating(4);
review.setTitle("Dune");
review.setAuthor("Frank Herbert");
return review;
}
@Test
void validReviewHasNoViolations() {
assertTrue(validator.validate(validReview()).isEmpty());
}
@Test
void ratingAboveFiveIsRejected() {
Review review = validReview();
review.setRating(9);
Set<ConstraintViolation<Review>> violations = validator.validate(review);
assertEquals(1, violations.size());
ConstraintViolation<Review> violation = violations.iterator().next();
assertEquals("rating", violation.getPropertyPath().toString());
assertEquals("{jakarta.validation.constraints.Max.message}", violation.getMessageTemplate());
}
@Test
void publishingRequiresAComment() {
Review review = validReview();
assertTrue(validator.validate(review).isEmpty());
assertEquals(1, validator.validate(review, Publish.class).size());
}
@Test
void isbnChecksumIsVerified() {
Review review = validReview();
review.setIsbn("978-0-306-40615-8");
assertEquals(1, validator.validateProperty(review, "isbn").size());
review.setIsbn("978-0-306-40615-7");
assertTrue(validator.validateProperty(review, "isbn").isEmpty());
}
}
The test needs junit-jupiter in the test scope and an Expression Language implementation, for instance
org.glassfish.expressly:expressly:6.0.0 (the @Isbn message uses ${validatedValue}). All four tests passed. The
validator under test is the very one that Faces uses, so a constraint that passes this test but fails in the form points
at the form (the property path, a converter, a group) and not at the rule. A constraint on a validator that injects a
CDI bean is tested with a stubbed field or with the validator’s constructor, since no container provides the injection
in a plain test.
Mojarra and MyFaces
The specification fixes the contract, and the scratch application was run on both implementations for the scenarios in
this page. The results were the same for the default validator, the labelled message format, the Default and Publish
groups, branch disabled, the validationGroups short-name failure (HTTP 500 with a FacesException on both),
f:validateWholeBean including the non-copyable and the placed-before-the-inputs failures, the CDI-injected validator,
and the localized messages. Mojarra and MyFaces differ in the following small respects only:
-
The MyFaces form needs its own hidden marker field when a request is replayed from a script (a field named like the form id plus
_SUBMIT). That affects only test scripts, not applications. -
The texts of exceptions differ. Only Mojarra’s wording of the
f:validateWholeBeanplacement and copy failures was recorded for this page.
Anything that depends on the wording of a Faces-generated message (the Validation Error: Value is required. text, the
converter message) belongs to the implementation’s message bundle, not to Bean Validation, and is overridden through the
<message-bundle> mechanism on both.
What Changed
| Version | Change |
|---|---|
JSF 2.0 (2009) |
Bean Validation integration is part of the specification: |
JSF 2.3 |
|
Faces 3.0 |
The packages become |
Faces 4.0 |
Jakarta EE 10, Bean Validation 3.0. The tag libraries use the |
Faces 4.1 |
Jakarta EE 11. Bean Validation is renamed Jakarta Validation 3.1, with clarified support for Java records (immutable records cannot be form targets, as an input must be able to write the property). |
Upcoming |
Jakarta Validation 4.0 and Faces 5.0 are in development: |
Where the Books Are Outdated
The older Faces books cover validation in the JSF 2.0 era, and the concepts they teach (that conversion precedes validation, that validators queue messages and short-circuit the lifecycle, that custom validators are classes or methods) still hold. Compared with the current specification:
-
Bean Validation 1.0 versus 3.1. JavaServer Faces 2.0: The Complete Reference targets JSF 2.0 and Bean Validation 1.0 (JSR 303). Packages are
javax.validationthere andjakarta.validationtoday, and the built-in list was short:@NotBlank,@NotEmptyand@Emailwere Hibernate extras then and are standard since Bean Validation 2.0.java.timesupport, container element constraints and repeatable annotations also came later. -
No
f:validateWholeBean. It arrived in 2.3, after all of the books, so cross-field validation there is done with custom validators or listeners.f:validateWholeBeanis the class-level alternative described above, and the validators page compares the options. -
Managed beans and custom validators. Books of that era use
@ManagedBeanbacking beans and<validator>registrations. Today beans are CDI@Namedand validators can be managed (injectable) with@FacesValidator(managed = true); for validators that belong to the model, a Jakarta ValidationConstraintValidatorwith CDI injection is preferred. -
The JSF 2.0 Cookbook. About half of its recipes rely on extension libraries (RichFaces, Tomahawk, Trinidad, Seam 2) that ended. The Faces core mechanism it shows for validators is unchanged; use the XHTML and the Java on this page with the current namespaces.
-
PrimeFaces client-side validation. The PrimeFaces Cookbook describes client-side validation that mirrors Bean Validation constraints in the browser. That is a PrimeFaces feature, covered on the PrimeFaces Ajax, forms and validation page of this section, and it is an addition to, not a substitute for, the server-side validation here.
These statements about the books rest on the scope and edition data recorded for the section’s bibliography and on the official specifications; the books themselves were consulted only for coverage. The official documentation wins on any point where they differ.
Source: Ed Burns, Chris Schalk and Neil Griffin, JavaServer Faces 2.0: The Complete Reference (McGraw-Hill, 2010, ISBN 978-0-07-162509-8) and Anghel Leonard, JSF 2.0 Cookbook (Packt, 2010, ISBN 978-1-847199-52-2), with the Jakarta Validation 3.1 specification
Common Mistakes
-
Short names in
validationGroups.validationGroups="Publish"fails at the first submit withValidation group not found. Use the fully qualified class name of the group interface. -
@NotNullon a text field, expecting "required". With the defaultINTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULLoffalse, the empty string passes. Use@NotBlank, or set the parameter totrue(Empty Values: Two Context Parameters That Decide Whether Constraints See Them). -
VALIDATE_EMPTY_FIELDSset tofalse. It turns off every presence constraint in the application. -
Forgetting
ENABLE_VALIDATE_WHOLE_BEAN.f:validateWholeBeanthen silently does nothing. -
Putting
f:validateWholeBeanbefore its inputs, or using a bean that cannot be copied. Both failed with exceptions on Mojarra and MyFaces. -
A class-level constraint in the
Defaultgroup. It would be evaluated against a bean that has not been populated yet, by every code path that validatesDefault. Put it in a dedicated group. -
Expecting constraints on a map entry, a literal, or a bean without annotations to be checked. A value expression with no constrained base class is skipped without a message.
-
@Validcascade expectations. Faces validates one property at a time; a cascading annotation is not consulted. -
Constraint messages that depend on the machine. A missing
_enbundle falls back to the JVM default locale, and a test that compares interpolated text fails on another workstation. -
Injecting into a constraint validator and getting
null. On servlet containers, publish a CDI-awareValidatorFactoryas the servlet context attribute (Injecting CDI Beans into Constraint Validators). -
Echoing sensitive input in messages.
${validatedValue}on a password or token field reveals what the user typed in the page and in logs. -
Treating client-side checks or Faces validation as the only line of defence. A request can bypass the page. Keep the constraints on the model and enforce them again at the service boundary.
-
Writing logic or database queries in a constraint validator. It runs for every validation call, including every input on every postback. Keep it cheap and do not call the persistence layer from it.
References
-
Jakarta Faces 4.1 specification, 3.5.6 Bean Validation Integration
-
Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
-
Jakarta EE Tutorial: Introduction to Jakarta Bean Validation
-
Burns, Schalk, Griffin: JavaServer Faces 2.0: The Complete Reference, McGraw-Hill, 2010, publisher page
-
Leonard: JSF 2.0 Cookbook, Packt, 2010, publisher page