Converters
|
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. |
Everything a browser sends is text, and everything a backing bean holds is a typed Java object. A converter is the
object that translates between the two: getAsObject() turns the submitted string into a model value, and
getAsString() turns a model value into the string that is written into the page. Without converters every bean
property would have to be a String. This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2,
part of Jakarta EE 11), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. The converter API
used below is the same in Faces 4.0 (Jakarta EE 10) apart from UUIDConverter, which is new in 4.1; the examples
were not run on 4.0. Faces 5.0 is still an
upcoming release (latest milestone 5.0.0-M9) and is not described as current behaviour. Every example and every
output quoted on this page was run for real: the files were deployed in a scratch Bookshelf WAR served by Tomcat
11.0.26 with Weld 6.0.4.Final and Mojarra 4.1.17 (JDK 26 with --release 21, English locale), the same files were
posted to MyFaces 4.1.4 where the text says so, and the review form was driven in a real browser (checked on
2026-10-08).
The running scenario is the review form of the Bookshelf: a reader enters the ISBN of a book, picks an author, rates the book, gives the date the book was read, the price paid and a few tags. Each of those fields needs a different converter, which makes the form a good tour of the whole topic.
Where Conversion Happens
Conversion is part of the request processing lifecycle (see the six phases) and it runs at both ends of a round trip:
-
Apply Request Values (phase 2). The renderer of each input component reads the string from the request and stores it unchanged as the component’s submitted value. Nothing is converted yet.
-
Process Validations (phase 3).
UIInput.validate()takes the submitted value and converts it into the local value by callinggetAsObject()(through the renderer’sgetConvertedValue()). Only if the conversion succeeds do the validators run, and they receive the converted object, not the string. -
Update Model Values (phase 4). The converted local value is written to the bean property, for example
review.setRating(Integer). This phase is skipped when any component of the executed part of the form has a conversion or validation error. -
Render Response (phase 6).
getAsString()runs on every output component and on every input component whose value is not being redisplayed from an error, and its result is written into the HTML.
Two consequences follow. A converter always sees the request’s locale: Faces requires converters that need a locale to
take it from the UIViewRoot of the current FacesContext, so changing the locale of the view changes how
numbers and dates are parsed and printed. And because validation happens on the converted value, the order of the
errors on a field is always "conversion first, validation second": a field that fails conversion never reaches its
validators.
The specification also asks converters to be symmetric: getAsObject(getAsString(x)) should give back a value
equal to x. When that is impossible (a java.text.Format that prints two adjacent numbers with no separator,
for example) the converter should at least document it.
How Faces Finds the Converter
For each UIInput and UIOutput component Faces uses the first converter it finds in this order:
-
A converter set explicitly on the component: the
converterattribute, a nestedf:converter,f:convertNumberorf:convertDateTimetag, orsetConverter()in Java. -
Otherwise, a converter registered for the type of the value expression. Faces calls
getType()on the value expression of thevalueattribute and asksApplication.createConverter(Class)for a converter registered for that class. -
If the type is
StringorObject, or there is no value expression, no conversion takes place and the string is used as it is.
The last box of the flowchart is the one that surprises people, and it behaves differently in the two implementations;
see Implicit Conversion. Select components follow the same rules, with one extra source of information for the converter:
the type of the values of their f:selectItem and f:selectItems children (see Entity Converters for Select Menus).
A plain expression in the page text is not a Faces component and never uses a converter. In the scratch run
<li>isbn=[#{reviewBean.isbn}]</li> printed the toString() of the record (Isbn[digits=9780306406157]), while
<h:outputText value="#{reviewBean.isbn}"/> printed 978-0-306-40615-7 because h:outputText is a UIOutput
and found the converter registered for Isbn.
The Standard Converters
Faces registers a converter for every Java type that a form commonly needs. They live in the
jakarta.faces.convert package, and each has a constant converter id (CONVERTER_ID):
| Class | Converter id | Registered by type for |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
by id only (the |
|
|
|
|
|
any |
|
|
|
|
|
|
|
|
|
|
|
by id only (the |
|
|
|
|
|
|
The tutorial’s list of converter ids stops at jakarta.faces.Short; the 4.1 specification adds
jakarta.faces.UUID, and the 4.1 API documents UUIDConverter with "Since: 4.1". The specification text also
says "java.lang.UUID" where it means java.util.UUID, which is the class the converter handles.
A compliant implementation additionally has to accept the registration of a converter for String.
Notice what is not in the table: LocalDate, LocalDateTime, Date, List, any entity or value object.
They have no converter by type, so you attach one (f:convertDateTime, your own converter) or the property
cannot be bound to an input field (see Implicit Conversion).
There are four ways to attach a converter to a component:
-
Do nothing, and rely on the type registration. This is the way most fields work.
-
Nest
f:convertNumberorf:convertDateTime, the two standard converters that have their own tag because they need configuration. -
Put the converter id in the
converterattribute:converter="jakarta.faces.Integer"orconverter="bookshelf.author". The attribute also accepts an EL expression that evaluates to aConverterinstance, which is how a converter exposed as a CDI bean is attached (see Custom Converters). -
Nest
f:converterwithconverterId(a converter id) orbinding(an EL expression that evaluates to aConverter, typically a bean property).
<!-- 1. by type: Integer property, IntegerConverter is chosen automatically -->
<h:inputText id="rating" value="#{review.rating}"/>
<!-- 3. the converter attribute with a converter id -->
<h:inputText id="pages" value="#{review.pages}" converter="jakarta.faces.Integer"/>
<!-- 4. f:converter with converterId, which also allows siblings such as f:validateLongRange -->
<h:inputText id="votes" value="#{review.votes}">
<f:converter converterId="jakarta.faces.Long"/>
</h:inputText>
The tutorial says the converter attribute accepts "the identifier or the fully-qualified class name" of the
converter. In the scratch run, converter="org.example.bookshelf.web.IsbnConverter" failed on both
implementations (Mojarra: Named Object: ... not found, MyFaces: Could not find any registered converter-class by
converterId), but that converter was registered only for its class with forClass. Use the converter id, or a
binding/EL expression, and do not rely on the class name.
Implicit Conversion
Implicit conversion is the by-type registration at work, and it is why a Integer bean property needs no tag at all.
These are the observed results of posting the Bookshelf test page (a request-scoped bean, Mojarra 4.1.17), with the
bean properties of the type shown:
| Property | Submitted | Result |
|---|---|---|
|
|
|
|
empty string |
|
|
|
error: "'abc' must be a number consisting of one or more digits." |
|
empty string |
|
|
|
error: out of the range of |
|
checkbox unchecked |
|
|
|
|
|
|
error: "'POETRY' must be convertible to an enum." |
|
|
the |
|
|
error: "'zzz' must be a UUID." |
|
empty string |
|
The last row is a classic source of bugs. Faces by default stores an empty text field in a String property as
"", so a check such as title == null never fires. Set the context parameter
jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL to true in web.xml and an empty field
becomes null (verified: with the parameter true the same post left title null, without it ""; see
the context parameters). Bean Validation’s @NotNull and
@NotBlank are the other way to express the rule; see the validators and Bean Validation pages.
A type with no converter
LocalDate, LocalDateTime and Date properties do not get a converter by type. If you bind a LocalDate
property to an input field without f:convertDateTime, the string cannot be assigned. What you see depends on the
implementation, and it is one of the most visible differences between them:
| Implementation | What happened for LocalDate readOn with the value 2026-10-07 |
|---|---|
Mojarra 4.1.17 |
A message on the page: "Conversion Error setting value '2026-10-07' for 'null Converter'." The same message appeared for an empty field. |
MyFaces 4.1.4 |
No message: the request ended with an error page, |
The fix is the same on both: attach f:convertDateTime with a java.time type or a custom converter, as shown
below.
f:convertNumber
f:convertNumber registers a NumberConverter (id jakarta.faces.Number) on the nearest parent component. It
formats and parses numbers, currencies and percentages with java.text.NumberFormat or a DecimalFormat pattern.
The attributes, from the 4.1 VDL documentation:
| Attribute | Meaning |
|---|---|
|
|
|
A |
|
A |
|
ISO 4217 code such as |
|
Symbol to print instead of the locale’s, used only for currencies |
|
Whether grouping separators are printed. Default |
|
Whether only the integer part is formatted and parsed. Default |
|
Bounds on the integer digits |
|
Bounds on the fraction digits |
|
A |
|
Used inside a composite component to name an attached object |
These outputs were produced by h:outputText with fixed numbers (default locale en_US):
| Tag | Output |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<h:outputText value="#{book.price}">
<f:convertNumber type="currency" currencyCode="EUR" locale="es_ES"/>
</h:outputText>
<h:inputText id="price" label="Price" value="#{review.paidPrice}">
<f:convertNumber pattern="#,##0.00" locale="en"/>
</h:inputText>
Parsing is where the details matter. These were verified with bean properties Double ratio, BigDecimal amount and
Long votes:
-
A
BigDecimalproperty withf:convertNumberreceives aBigDecimal(1,234.567became1234.567). The converter parses to aDoubleorLongand the value is coerced to the property type, so for money preferBigDecimaland apattern, and be aware of the intermediatedouble. -
type="percent"parses25%to0.25and rejects0.25("could not be understood as a percentage") and, with a space,25 %. -
type="currency"requires the currency symbol: withlocale="es_ES"andcurrencyCode="EUR",1.234,50 €(with a normal or a no-break space) was accepted and1234,5or12 EURwas rejected. -
integerOnly="true"did not change the formatted output of12.99(it printed12.99), but when parsing12.99into aLongthe result was12. In practice it is a parse option. -
maxFractionDigitsis a formatting bound.1,234.567was accepted withmaxFractionDigits="2"on input. -
Mojarra parses leniently, MyFaces strictly. With Mojarra
12abcwas accepted as12, and1.234,5in theenlocale was accepted as1.234, silently losing the rest. MyFaces rejected both with "is not a number". Do not rely on the converter alone to reject trailing text: add af:validateRegexor a pattern check, or write a strict custom converter, when the input must be exact.
f:convertDateTime and java.time
f:convertDateTime registers a DateTimeConverter (id jakarta.faces.DateTime). Since Faces 2.3 it handles the
java.time types as well as java.util.Date; the type attribute chooses the formatter:
type |
Property type | Default format when no pattern is given |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The other attributes are dateStyle and timeStyle (default, short, medium, long, full; with a java.time
formatter default means medium), pattern, locale, timeZone, binding and for. When pattern is set,
dateStyle, timeStyle and (for java.util.Date) type are ignored; for the java.time types the pattern is passed
to DateTimeFormatter.ofPattern(pattern, locale), and for java.util.Date to SimpleDateFormat. Parsing is
non-lenient: the string must match the format exactly.
Verified outputs (a Date of the epoch, rendered with timeZone="UTC", and LocalDate 2026-10-07):
| Tag | Output |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<h:inputText id="readOn" label="Date read" value="#{review.readOn}">
<f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/>
</h:inputText>
<h:outputText value="#{book.publishedAt}">
<f:convertDateTime type="localDate" dateStyle="long"/>
</h:outputText>
Four traps appear with dates, all verified:
-
The localized time formats contain a narrow no-break space. With JDK 20 and later,
dateStyle="medium"withtimeStyle="short"in theenlocale prints3:15 PMwith U+202F between the time andPM, and the time-only format printed12:00:00 AMthe same way. A user who types a normal space gets "could not be understood as a date and time" (the example the converter prints in the error message contains the same invisible character). Use apatternfor anything a person has to type; use the styles only for output. -
The default time zone is GMT, not the server’s. A
java.util.Dateof the epoch printed1970-01-01 00:00 GMTwith notimeZoneattribute, and1970-01-01 01:00 CETwithtimeZone="Europe/Madrid", on a JVM whose own zone was CEST. Faces defaults toGMTforDateconversion because the specification says so. Setjakarta.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONEtotrueinweb.xmland the same tag withouttimeZoneprinted1970-01-01 01:00 CET. Thejava.timetypes carry their own zone information and are not affected. -
type="date"cannot format aLocalDate.<f:convertDateTime type="date"/>on aLocalDateended inConverterException: ... Could not convert '2026-10-07' to a string, caused byIllegalArgumentException: Cannot format given Object as a Date. Match thetypeto the property type. -
h:outputTextof aLocalDatewith no converter printstoString()(2026-10-07) and does not fail; it is only the input direction that has no converter.
UUIDConverter and EnumConverter
UUIDConverter is new in Faces 4.1 (java.util.UUID properties, id jakarta.faces.UUID). A UUID bean
property bound to an input field needs no tag: the value 123e4567-e89b-12d3-a456-426614174000 was converted, and
zzz was rejected with "'zzz' must be a UUID. Example: 29573f55-4254-4afa-9ca6-6b5ae6c7ab6e". In Faces 4.0 and
earlier the same field needs a custom converter.
EnumConverter has been available since 1.2 and converts any enum to its name() and back. It is the converter
behind a menu of constants:
public enum Genre {
FICTION, SCIENCE, HISTORY
}
<h:selectOneMenu id="genre" value="#{review.genre}">
<f:selectItem itemLabel="Any" noSelectionOption="true"/>
<f:selectItems value="#{review.genres}"/>
</h:selectOneMenu>
With getGenres() returning Genre.values() the menu rendered options whose values are the constant names
(FICTION, SCIENCE, HISTORY). Posting POETRY (a value the menu never offered) produced the message
"'POETRY' must be convertible to an enum from the enum that contains the constant 'POETRY'", which is also what a
tampered request looks like. Labels come from itemLabel (or toString() of the constant): write the display text
in a resource bundle, not in the enum’s name(), because name() is the value that travels in the form.
The upcoming Faces 5.0 lists, among its planned issues, generics for the standard converters and an EnumConverter
that finds the enum type from the value expression. Both are planned changes in a milestone, not current behaviour.
Source: UUIDConverter (since 4.1) and EnumConverter
Custom Converters
When the standard converters do not know your type, write your own. A converter is a class that implements
jakarta.faces.convert.Converter<T> and its two methods:
-
T getAsObject(FacesContext context, UIComponent component, String value), called in Process Validations. It must returnnullfor anullor empty string (an empty optional field is not an error) and throw aConverterExceptionfor text it cannot convert. -
String getAsString(FacesContext context, UIComponent component, T value), called in Render Response. It must return an empty string fornull.
The interface in 4.1 is generic. The class must have a public no-argument constructor (or, for a per-class converter,
one that takes a Class, which is how EnumConverter receives its enum type), and a new instance is created
wherever the converter is used. The specification says converters need not be thread-safe because they are only used
inside one request thread, and it follows that a converter exposed as an @ApplicationScoped bean (see below) must
be written to be stateless.
The Bookshelf identifies a book by its ISBN-13, which the application models as a small value type:
public record Isbn(String digits) implements Serializable {
/** Formats the digits as 978-0-306-40615-7 (display only). */
public String formatted() {
return digits.substring(0, 3) + "-" + digits.substring(3, 4) + "-"
+ digits.substring(4, 7) + "-" + digits.substring(7, 12) + "-" + digits.substring(12);
}
}
@FacesConverter(forClass = Isbn.class)
public class IsbnConverter implements Converter<Isbn> {
@Override
public Isbn getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isBlank()) {
return null;
}
String digits = value.replaceAll("[-\\s]", "");
if (!digits.matches("\\d{13}")) {
FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Not an ISBN-13", "'" + value + "' must contain exactly 13 digits (hyphens are allowed).");
throw new ConverterException(message);
}
return new Isbn(digits);
}
@Override
public String getAsString(FacesContext context, UIComponent component, Isbn value) {
return value == null ? "" : value.formatted();
}
}
With forClass the converter is registered by type, so every Isbn property in the application uses it with no
tag at all, in input fields and in h:outputText. Run on the form, + 978-0-306-40615-7 ` converted to
`+Isbn[digits=9780306406157], the stored value was shown again as 978-0-306-40615-7, and 12345 produced the
message "Not an ISBN-13" with the detail text above.
Because the converter has no dependency on the container, a unit test needs no Faces runtime. The FacesContext and
UIComponent arguments can be null when the converter does not use them (a converter that reads component
attributes needs a real or mocked component):
class IsbnConverterTest {
private final IsbnConverter converter = new IsbnConverter();
@Test
void stripsSeparators() {
Isbn isbn = converter.getAsObject(null, null, " 978-0-306-40615-7 ");
assertEquals("9780306406157", isbn.digits());
}
@Test
void emptyIsNull() {
assertNull(converter.getAsObject(null, null, ""));
}
@Test
void roundTrip() {
Isbn isbn = converter.getAsObject(null, null, "9780306406157");
assertEquals("978-0-306-40615-7", converter.getAsString(null, null, isbn));
}
@Test
void rejectsShortNumbers() {
ConverterException e = assertThrows(ConverterException.class,
() -> converter.getAsObject(null, null, "12345"));
assertEquals("Not an ISBN-13", e.getFacesMessage().getSummary());
}
}
The same four calls were run in a main method against the compiled converter, and printed
Isbn[digits=9780306406157], null, 978-0-306-40615-7 and Not an ISBN-13 | ERROR 2; the JUnit 5 form shown
here is the same code with assertions; the testing page compiles and runs the same four tests (all passed) and shows how to test a converter that reads component attributes.
Source: ConverterException API and Converter API
Registering a converter
There are three registrations. The API documentation says the annotation ends in an Application.addConverter() call
at start-up, which is what the XML element configures too:
-
@FacesConverter("bookshelf.tagList")registers the class by id. Components refer to it withconverter="bookshelf.tagList"or<f:converter converterId="bookshelf.tagList"/>. -
@FacesConverter(forClass = Isbn.class)registers it by type, as above. It is used automatically for properties of that class. -
The
faces-config.xmlelement<converter>with<converter-id>or<converter-for-class>, and<converter-class>(the 4.1 schema also has<converter-extension>). It replaces the annotation and is the only way to configure a converter whose class you cannot annotate. See the project setup page for the file.
One annotation cannot do both by id and by type. The API documentation says so, and suggests putting the logic in an
abstract base class and deriving two classes, one with value and one with forClass.
<converter>
<converter-id>bookshelf.tagList</converter-id>
<converter-class>org.example.bookshelf.web.TagListConverter</converter-class>
</converter>
Source: FacesConverter API and web-facesconfig_4_1.xsd
Converters with parameters
A converter often needs a setting that differs per field. There are three ways to give it one.
An attribute of the component. The converter reads component.getAttributes(), and the page author sets the value
with f:attribute inside the component. This keeps the converter class independent of any bean:
@FacesConverter("bookshelf.tagList")
public class TagListConverter implements Converter<List<String>> {
private static final String SEPARATOR_ATTRIBUTE = "separator";
@Override
public List<String> getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isBlank()) {
return List.of();
}
String separator = separator(component);
return Arrays.stream(value.split(Pattern.quote(separator)))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}
@Override
public String getAsString(FacesContext context, UIComponent component, List<String> value) {
return value == null ? "" : String.join(separator(component) + " ", value);
}
private String separator(UIComponent component) {
Object configured = component.getAttributes().get(SEPARATOR_ATTRIBUTE);
return configured == null ? "," : configured.toString();
}
}
<h:inputText id="tags" value="#{review.tags}">
<f:converter converterId="bookshelf.tagList"/>
<f:attribute name="separator" value=";"/>
</h:inputText>
Posting sci-fi; classics ; space to this field gave the list [sci-fi, classics, space].
A property of the converter. A converter can also have JavaBean properties, which is how NumberConverter and
DateTimeConverter take their pattern and locale. The tags f:convertNumber and f:convertDateTime set
those properties. A converter with configuration that must survive across requests should also implement
StateHolder (or PartialStateHolder), because the converter instance belongs to the component tree and its
state is saved with the view (see the page on state saving).
A converter that is a CDI bean. Make the converter a named bean and attach it with an EL expression. The expression
may also be used in f:converter binding:
@Named("slugConverter")
@ApplicationScoped
public class SlugConverter implements Converter<String> {
@Override
public String getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null) {
return null;
}
return value.trim().toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "-").replaceAll("(^-|-$)", "");
}
@Override
public String getAsString(FacesContext context, UIComponent component, String value) {
return value == null ? "" : value;
}
}
<h:inputText id="slug" value="#{review.slug}" converter="#{slugConverter}"/>
The input + The Left Hand of Darkness! ` was stored as `+the-left-hand-of-darkness. The bean is shared by every
request, so it holds no per-request state. When the converter needs to look something up, prefer the managed
converter of the next section, which is created and injected by CDI and is not shared.
Managed Converters and CDI Injection
A converter instantiated by Faces is not a CDI bean, so @Inject does not work inside it. Run on Mojarra 4.1.17, a
converter declared with the plain @FacesConverter("plainInjected") and an @Inject AuthorService field was created
and called, but the field was null: the converter returned its marker for "not injected". The 4.1 API provides the
fix, the managed element of the annotation: "an indicator that flags whether or not the given converter is a CDI
managed converter". A managed converter is created and injected by CDI. The Bookshelf uses one to turn an author id from
the form into an Author:
public record Author(Long id, String name) implements Serializable { }
@ApplicationScoped
public class AuthorService {
private final List<Author> authors = List.of(
new Author(1L, "Ursula K. Le Guin"),
new Author(2L, "Italo Calvino"),
new Author(3L, "Stanislaw Lem"));
public List<Author> findAll() {
return authors;
}
public Optional<Author> findById(long id) {
return authors.stream().filter(a -> a.id() == id).findFirst();
}
}
@Dependent
@FacesConverter(value = "bookshelf.author", managed = true)
public class AuthorConverter implements Converter<Author> {
@Inject
private AuthorService authors;
@Override
public Author getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isBlank()) {
return null;
}
try {
return authors.findById(Long.parseLong(value)).orElseThrow(
() -> new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Unknown author", "No author has the id " + value)));
} catch (NumberFormatException e) {
throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Invalid author id", "'" + value + "' is not an author id"), e);
}
}
@Override
public String getAsString(FacesContext context, UIComponent component, Author value) {
return value == null ? "" : String.valueOf(value.id());
}
}
The @Dependent line is not decoration. With bean-discovery-mode="annotated" in beans.xml (the mode
the section’s project uses), @FacesConverter is a CDI qualifier and not a bean-defining annotation, so the class is
not a bean until you give it a scope. Without it:
-
Mojarra reported
Error - @FacesConverter("bookshelf.author", managed=true) on ...AuthorConverter could not be resolved by CDI. Check that beans.xml is present and that the BeanManager is fully initialized before FacesConverter instances are created. -
MyFaces 4.1.4 did not report that message: the page failed at render time with a
NullPointerExceptioninFacesConverterCDIWrapper.getAsStringbecause the wrapped converter wasnull.
Add @Dependent (a new instance per use, as for ordinary converters), or use bean-discovery-mode="all". Both
implementations then behaved the same. Forgetting the scope is the most common reason a managed converter "does not
work"; managed validators are covered on the validators page.
A managed converter can be registered by id (as here, converter="bookshelf.author") or by type
(@FacesConverter(forClass = Author.class, managed = true)); the by-type form worked for a selectOneMenu and for
a selectManyCheckbox bound to a List<Author> with no converter attribute at all, on Mojarra.
Entity Converters for Select Menus
Select components render a list of strings and need to map the chosen string back to the object. The selectOneMenu
for the author is the standard case of an entity converter: the value of each f:selectItem is the object, the
rendered option value is getAsString() (the id), and getAsObject() finds the object again from the id. How the lookup reaches a database (and why it needs a service, not a static DAO) is on the persistence page.
<h:selectOneMenu id="author" label="Author" value="#{review.author}" converter="bookshelf.author" required="true">
<f:selectItem itemLabel="Choose an author" noSelectionOption="true"/>
<f:selectItems value="#{review.authors}" var="a" itemValue="#{a}" itemLabel="#{a.name}"/>
</h:selectOneMenu>
The menu rendered <option value="1">Ursula K. Le Guin</option> and so on, and posting 2 set the property to the
Author with id 2.
For a List<Author>, a selectManyCheckbox or selectManyListbox has a problem: the generic type of the
property is erased at run time, so Faces cannot ask the property for its element type. The API documentation for
h:selectManyListbox calls it out as a subtlety of converters and collections. Name the converter on the component
(converter="bookshelf.author") or register one by type. Without any converter, the selectManyCheckbox test
failed with two messages: "Conversion Error setting value '2' for 'null Converter'." for a single select, and
"Validation Error: Value is not valid" for the many-select, because the posted strings no longer matched the item values.
Four rules make entity converters safe:
-
Convert to a key, not to an object graph.
getAsString()returns the id, never a serialised entity or atoString()meant for people. Whatever the page shows is also visible and changeable by the browser, so the converter must treat the string as untrusted input. -
Validate the key. An unknown id raised a
ConverterException("Unknown author") and a non-numeric one another ("Invalid author id"), instead of aNullPointerExceptionorNumberFormatExceptionreaching the user. -
Check authorisation where it matters. Anyone can post any id. If a user may only choose some authors, the converter or a validator must check it; the fact that the menu offered only those authors is not a protection.
-
Keep
getAsString()free of queries. It runs for every option of every menu in Render Response and should only read the id from the object it is given. The lookup ingetAsObject()runs once per submitted value.
For a generic converter that serves every entity of an application (the usual answer to "I have forty entity classes"), the OmniFaces library offers ready-made select-item converters; they are covered with that library, on the OmniFaces page.
The tutorial’s Duke’s Tutoring case study registers @FacesConverter(forClass = Guardian.class, value = "guardian")
and says the forClass attribute makes it the default converter for Guardian. The API documentation says the
opposite: when value is not empty, only the id registration is made. That is what happened in the scratch run: with
@FacesConverter(value = "bookshelf.author", forClass = Author.class, managed = true) and no converter attribute,
the selectOneMenu failed with "Conversion Error setting value '2' for 'null Converter'", so the by-type
registration had not been made. Use the id and the attribute, or forClass alone.
Conversion Errors and Messages
A converter reports failure by throwing jakarta.faces.convert.ConverterException. The constructors take a
String or a FacesMessage (and optionally a cause). Prefer the FacesMessage form: it carries a severity, a
summary and a detail. Faces catches the exception in UIInput.validate(), queues the message on the
FacesContext for the client id of the component, marks the component invalid and moves on to Render Response, as
the diagram at the start of this page shows. Four things are worth knowing:
-
The component keeps what the user typed. After a failed conversion the field is redisplayed with the submitted string and not with the old model value. That is why a form that fails to convert still shows the wrong input for the user to correct.
-
The model, the validators and the action are skipped. The bean setters and the
actionmethod were not called in the failed posts of the scratch run (the action’s flag stayedfalse); this is the usual "my button does nothing" cause. Always put anh:messagesor anh:messagewhere the user can see the error. -
Only
ConverterExceptionis a conversion error. A converter that lets aNumberFormatExceptionescape (a deliberately faulty test converter calledInteger.parseIntonxx) produced the "An Error Occurred" page of the Development stage with the textFor input string: "xx", not a field message. Catch and wrap. -
A failing
getAsStringis not recoverable. It runs in Render Response, where the exception ends in the error page (thetype="date"on aLocalDateexample above). Converters should accept any value they were handed.
The message of a standard converter
Standard converters produce their messages from the Faces message resource, with a summary and a detail. These are
the keys, and in each message {0} is the submitted value, {1} an example value, and {2} the label of the
component (for some converters {1} is the label, and the placeholders are listed in the Javadoc of each message
constant):
| Key | Default text (Mojarra 4.1.17, English) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The keys have the same shape for every converter (jakarta.faces.converter.<Class>.<ID> with a _detail
variant), and the other converters follow it. The label {2} is the component’s label attribute when it has one and
the client id otherwise: the failed post printed f:pages: "y" is not a whole number... before a label="Page
count" was added and Page count: "y" is not a whole number... after. Always set label on inputs so the messages
read well.
Changing the message
There are three levels, from the narrowest to the widest.
One component: converterMessage. Every input component has the attributes converterMessage, validatorMessage
and requiredMessage. When set, converterMessage replaces the message of any converter failure on that component,
the standard converters' and your own custom converter’s alike (it replaced the summary and the detail, so the text
appeared twice with showSummary and showDetail both on):
<h:inputText id="rating" label="Rating" value="#{review.rating}"
converterMessage="The rating must be a whole number."/>
<h:message for="rating"/>
The whole application: a message bundle. Register a bundle with <message-bundle> in faces-config.xml and
define the keys above to replace the default texts everywhere. The file below made the failed post print
Page count: "y" is not a whole number. Type digits only, for example 9346.:
jakarta.faces.converter.IntegerConverter.INTEGER={2}: "{0}" is not a whole number.
jakarta.faces.converter.IntegerConverter.INTEGER_detail=Type digits only, for example {1}.
<application>
<message-bundle>org.example.bookshelf.messages</message-bundle>
</application>
For a Spanish bundle, the name is the same with a locale suffix (messages_es.properties), and the messages and
internationalisation page of this section shows English and Spanish files side by side. <message-bundle> is for overriding the messages of
the Faces implementation; the bundle you read from the page with <resource-bundle> or var is a different thing.
A custom converter: the message in the exception. Build the FacesMessage yourself, as the converters above do, and
read the text from a bundle if the application is translated. A ConverterException created with only a string gets
that string as the message.
Conversion errors with Ajax
The review form is submitted with f:ajax (the Ajax page explains execute and render). The lifecycle is the same, except that only the components named in
execute are decoded and converted, and the messages reach the page only if a message component is in render.
With a h:message and h:messages inside the form, the form below re-renders both on every submit. A raw
partial request for rating=abc returned the update element <span id="f:m">Type digits only, for example
9346.</span> for the h:message and a ul with the same text for h:messages. A request that listed only
@form in the hand-built jakarta.faces.partial.execute did not convert the field, so test with a real browser or a
test client that runs faces.js, not with hand-made requests.
The Bookshelf Review Form
This is the form the rest of the section builds on: the converters from this page combined with the template of the templating page. The bean is request-scoped to keep the example short; the scopes page explains why a real Ajax form uses a view-scoped bean.
@Named("review")
@RequestScoped
public class ReviewFormBean {
@Inject
private AuthorService authorService;
private Isbn isbn;
private Author author;
private Genre genre;
private Integer rating;
private LocalDate readOn;
private BigDecimal paidPrice;
private List<String> tags = List.of();
private String summary;
public List<Author> getAuthors() {
return authorService.findAll();
}
public Genre[] getGenres() {
return Genre.values();
}
public String submit() {
// In the real application the review is stored here.
summary = "Saved review of " + isbn.formatted() + " by " + author.name() + ", rating " + rating
+ ", read on " + readOn + ", paid " + paidPrice + ", tags " + tags + ", genre " + genre;
return null;
}
// getters and setters for isbn, author, genre, rating, readOn, paidPrice, tags and summary
}
<ui:composition template="/WEB-INF/templates/layout.xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<ui:define name="title">New review</ui:define>
<ui:define name="content">
<h:form id="reviewForm">
<h:messages id="messages" globalOnly="true" showDetail="true"/>
<h:outputLabel for="isbn" value="ISBN"/>
<h:inputText id="isbn" label="ISBN" value="#{review.isbn}" required="true"/>
<h:message for="isbn"/>
<h:outputLabel for="author" value="Author"/>
<h:selectOneMenu id="author" label="Author" value="#{review.author}" converter="bookshelf.author" required="true">
<f:selectItem itemLabel="Choose an author" noSelectionOption="true"/>
<f:selectItems value="#{review.authors}" var="a" itemValue="#{a}" itemLabel="#{a.name}"/>
</h:selectOneMenu>
<h:message for="author"/>
<h:outputLabel for="genre" value="Genre"/>
<h:selectOneMenu id="genre" value="#{review.genre}">
<f:selectItem itemLabel="Any" noSelectionOption="true"/>
<f:selectItems value="#{review.genres}"/>
</h:selectOneMenu>
<h:outputLabel for="rating" value="Rating (1 to 5)"/>
<h:inputText id="rating" label="Rating" value="#{review.rating}"
converterMessage="The rating must be a whole number."/>
<h:message for="rating"/>
<h:outputLabel for="readOn" value="Read on (yyyy-MM-dd)"/>
<h:inputText id="readOn" label="Date read" value="#{review.readOn}">
<f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/>
</h:inputText>
<h:message for="readOn"/>
<h:outputLabel for="price" value="Price paid"/>
<h:inputText id="price" label="Price" value="#{review.paidPrice}">
<f:convertNumber pattern="#,##0.00" locale="en"/>
</h:inputText>
<h:message for="price"/>
<h:outputLabel for="tags" value="Tags (separated by semicolons)"/>
<h:inputText id="tags" value="#{review.tags}">
<f:converter converterId="bookshelf.tagList"/>
<f:attribute name="separator" value=";"/>
</h:inputText>
<h:commandButton id="save" value="Save review" action="#{review.submit}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<h:outputText id="result" value="#{review.summary}"/>
</h:form>
</ui:define>
</ui:composition>
Source: Jakarta EE Tutorial: Using the Standard Converters and Jakarta EE Tutorial: Using Ajax with Jakarta Faces
The form was loaded in a real browser (the Ajax script ran) and submitted twice. The first, invalid submission
(12345 in ISBN, no author, x as rating, 07/10/2026 as date, abc as price) re-rendered the form with:
-
ISBN:'12345' must contain exactly 13 digits (hyphens are allowed). -
Author:Author: Validation Error: Value is required.(the "Choose an author" option is a no-selection option whose value is the empty string, sorequiredfires) -
Rating:The rating must be a whole number. -
Date read:Date read: '07/10/2026' could not be understood as a date. Example: 2026-10-08 -
Price:Price: 'abc' is not a number pattern. Example: #,##0.0#
and the second, valid one (978-0-306-40615-7, author 2, rating 5, 2026-10-07, 1,234.50, tags
sci-fi; classics) ended with "Saved review of 978-0-306-40615-7 by Italo Calvino, rating 5, read on 2026-10-07, paid
1234.50, tags [sci-fi, classics], genre null". The messages for the numeric-pattern failure show the pattern as the
"example", which can confuse readers; set converterMessage on fields whose pattern is not obvious.
Everything that touches security in this form is on the server (see the security page for output escaping): the author id and the ISBN are untrusted input that
the converters parse and check, the page never prints a user-supplied value with escape="false", and the action
only runs when conversion and validation succeed.
Converters in Other Places
-
h:outputText. It is aUIOutputand uses the same lookup, sof:convertNumberandf:convertDateTimeare the way to format numbers and dates for display. Do not build the text in a getter that returns a pre-formatted string, which defeats the locale handling. -
f:viewParam. A view parameter has the attributesconverterandconverterMessage(VDL documentation), so a bookmarkable?isbn=...URL can be turned into anIsbn; the view parameters page covers it. -
Composite components.
f:convertNumber,f:convertDateTimeandf:converterhave aforattribute that attaches the converter to a named target of the composite component, not to the composite itself (composite components). -
Programmatic access.
FacesContext.getCurrentInstance().getApplication().createConverter(Isbn.class)andcreateConverter("bookshelf.author")return a configured instance by type or by id, which is how code outside the view (an exception handler, a REST endpoint) can reuse the same parsing as the form. A UI component accepts a converter fromsetConverter(). -
Client behaviours and Ajax events. A
valueChangeevent of an input is delivered only after conversion and validation succeeded; a field with a conversion error never fires its listener (the events page covers listeners). -
Validators. They receive the converted value. A validator on
Isbntherefore checks a checksum, not text, and the page on validators showsf:validateRegexfor fields kept as strings.
Mojarra and MyFaces
The two implementations pass the same specification tests, and for converters they agree on the contract. The differences below were all observed by running the same files on Mojarra 4.1.17 and MyFaces 4.1.4, and they are the reason an application should be tested on the implementation it deploys:
| Situation | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
Field message "Conversion Error setting value … for 'null Converter'" |
Unhandled |
Number with trailing text, |
Accepted as |
Rejected: "is not a number" |
|
Accepted as |
Rejected |
Managed converter without a bean-defining annotation |
Clear error text that mentions |
|
Default messages for the same errors |
Same keys |
Same keys, different example values ( |
Behaviour the two shared in the runs: the formatted output of every f:convertNumber and f:convertDateTime row
above, converterMessage, a managed converter with @Dependent used by id on a menu, the custom ISBN, tag list
and author converters, and a converter that is a CDI bean attached with an EL expression. The GMT default, the
label text and the by-type menu lookup were run on Mojarra only.
What Changed Between Versions
| Version | Change relevant to converters |
|---|---|
JSF 1.2 |
|
JSF 2.0 |
|
JSF 2.3 |
|
Faces 3.0 |
|
Faces 4.0 |
The namespaces become |
Faces 4.1 |
|
Faces 5.0 (upcoming, milestone |
The planned work lists generics for the standard converters and an |
The 2.3 row follows the section’s version page; the 2.0 row and the
point at which Converter became generic were not independently checked.
Where the Books Are Outdated
Conversion is covered in nearly every Faces book: the converter chapters of JavaServer Faces 2.0: The Complete Reference (Burns and Schalk), the converter recipes of the JSF 2.0 Cookbook (Leonard, including converters with parameters and converters bound to beans), the conversion and validation chapters of Mann’s JavaServer Faces in Action and of Dudney et al. The central idea (a converter maps between the string in the page and the object in the model, in both directions, in two lifecycle phases) has not changed since the first edition. These points have:
-
Packages and ids. The books use
javax.faces.convert.Converter. The current interface isjakarta.faces.convert.Converter, the current ids start withjakarta.faces.(jakarta.faces.Integer), and the namespace of the tags isjakarta.faces.core(the JSP<%@ taglib uri="http://java.sun.com/jsf/core" %>of the early books is gone). -
Registration in XML. In the JSF 1.x books a custom converter is registered in
faces-config.xmland needs the<converter-for-class>or<converter-id>element. The annotation is the normal way now, and the XML remains valid. -
No
java.time. The books convertjava.util.DateandSimpleDateFormat. Modern code keepsLocalDateand friends and usesf:convertDateTime type="localDate"; theDatepath keeps its GMT default. -
"Converter with a parameter" recipes. Leonard and others use a managed bean,
@ManagedPropertyor a lookup fromFacesContextto get a service into a converter. Today that is@Injectin amanaged = trueconverter, and a service lookup byELContextis never needed. -
Entity converters. The older books often convert an entity by serialising it in
getAsString()or by calling a static DAO. Convert to the key and look the entity up through an injected service, as shown above. -
Ajax. They describe a hand-written refresh for a failed conversion; with
f:ajaxthe messages are components inrender. -
No
UUIDConverter, which is new in 4.1.
Common Pitfalls
-
A
LocalDateorDatefield with no converter. It fails in the model update (Mojarra) or with an unhandledELException(MyFaces). Always attachf:convertDateTimeor a converter. -
A
Stringproperty that is""instead ofnull. Setjakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULLor test for blank. -
Lenient number parsing in Mojarra.
12abcbecomes12. Validate, or use a strict custom converter. -
The GMT default for
java.util.Date. SettimeZoneor the system-timezone context parameter, or usejava.time. -
The narrow no-break space in localized times. Do not ask users to type a style format; give them a pattern.
-
Throwing anything but
ConverterException. Wrap parsing errors. -
Forgetting
@Dependenton a managed converter when CDI uses annotated discovery. -
Expecting
@Injectin an ordinary@FacesConverter. It staysnull. -
Trusting the id of an entity converter. Check that the user may choose that entity.
-
A converter with per-request fields registered as
@ApplicationScoped. It is shared by all threads. -
converter="some.package.Class". Use the converter id; the class name failed on both implementations. -
No
label. Messages show the client id (reviewForm:rating) and not a human name. -
#{...}in page text. It does not use a converter; useh:outputTextfor formatted output.
References
-
Converter, FacesConverter, ConverterException, UUIDConverter, EnumConverter, DateTimeConverter and NumberConverter in the Jakarta Faces 4.1 API documentation
-
f:convertNumber, f:convertDateTime and f:converter in the Jakarta Faces 4.1 VDL documentation
-
JavaServer Faces 2.0: The Complete Reference, Burns and Schalk, McGraw-Hill (publisher page)
-
JavaServer Faces in Action, Mann, Manning (publisher page, out of print)