Type Conversion

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Struts 7.4.0 (released 2026-10-02) running on Apache Tomcat 11.0.26 and Java 21, the combination on which every example was built, deployed and requested; the facts were checked on 2026-10-08 and 2026-10-10 against the Core Developer Guide pages on https://struts.apache.org/core-developers/, the 7.4.0 Javadoc, the source files tagged STRUTS_7_4_0 and the classes inside the 7.4.0 struts2-core jar (javap). HTTP carries text and nothing else: a form field named pages arrives as the string "12", a date arrives as whatever the user typed, a checkbox arrives as "on" or not at all. Type conversion is the part of Struts that turns those strings into the int, BigDecimal, Date, enum, List or domain object that the action property declares, and turns the property back into a string when a form or a result renders it. Most of the time it is invisible; this page is about the moments when it is not: which formats are accepted, how the request locale changes them, how to teach Struts a new type, how collections and maps of beans are filled from indexed parameter names, and what happens, and what the visitor sees, when a value cannot be converted. The running scenario is the Bookshelf catalogue of this part: a book has a page count, a publication date, a list price, an ISBN and a shelf code. The related pages of this section are ValueStack, ActionContext & OGNL, Parameter Injection and @StrutsParameter, Validation, Localization and i18n and ModelDriven, Preparable & CRUD.

Where Conversion Happens

Conversion is not an interceptor. It is a service that the OGNL layer calls whenever it assigns a value to a property whose type differs from the type of the value. The params interceptor reads the request parameters (the interceptor stack page lists where it sits in defaultStack), asks the value stack to set each one on the action, and the value stack asks the converter to produce a value of the right type. When the converter cannot, it does not throw to the caller: it records the failure in a map called conversionErrors of the ActionContext and moves on to the next parameter. A later interceptor, conversionError, converts those records into field errors, and workflow decides, from the presence of field errors, that the input result must render. The same machinery runs in the other direction when a tag such as <s:textfield name="listPrice"/> reads the property back: the value is converted to a string for the page.

flowchart TD req["POST /edit.action
pages=abc, isbn=978-0-13-468599-1"] --> params["params interceptor
one setValue per parameter name"] params --> ognl["OGNL on the value stack
setPages(...), setIsbn(...)"] ognl --> lookup{"Converter for this
property or type?"} lookup -->|"property rule in Action-conversion.properties
or Class-conversion.properties"| custom["Custom converter
(StrutsTypeConverter)"] lookup -->|"type rule in struts-conversion.properties"| custom lookup -->|"neither"| basic["Built-in converters
(XWorkBasicConverter, NumberConverter, DateConverter, ...)"] custom -->|"value"| set["property is set"] basic -->|"value"| set custom -->|"exception"| err["conversionErrors map in ActionContext
(property name, rejected text, target type)"] basic -->|"exception"| err err --> cei["conversionError interceptor
addFieldError + original value kept"] cei --> val["validation interceptor
(conversion validator, validate())"] val --> wf["workflow interceptor
field errors present: return input"] wf --> view["input result renders
fielderror, form shows the original text"]

The picture shows the three facts that the rest of the page keeps returning to. First, a conversion failure is data, not an exception: the action method still runs unless a later interceptor stops it. Second, the failure is only reported if conversionError (or the conversion validator) is in the stack and the action implements ValidationAware (every ActionSupport does). Third, the rejected text is kept, so that the form can show abc again instead of 0.

The classes involved in 7.4.0 live in the package org.apache.struts2.conversion (the guide pages still show com.opensymphony.xwork2 names in places; the package was renamed in 7.0.0, see Where the Books Are Outdated).

Table 1. The main types of the conversion subsystem (read from the 7.4.0 jar)
Type Role

org.apache.struts2.conversion.TypeConverter

The interface of a converter: convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType); the constant NO_CONVERSION_POSSIBLE is the signal for "not my type"

org.apache.struts2.conversion.impl.XWorkConverter

The registry and dispatcher: reads the conversion properties files, finds the converter for a property or a type, records conversion errors

org.apache.struts2.conversion.impl.XWorkBasicConverter

The built-in converter for boolean, char, numbers, dates, enums, arrays, collections, Class and String; delegates numbers to NumberConverter and dates to DateConverter

org.apache.struts2.util.StrutsTypeConverter

The helper base class for converters written by applications: two methods, convertFromString and convertToString

org.apache.struts2.conversion.TypeConversionException

The exception a converter throws to say "this text is not a valid value"

org.apache.struts2.conversion.ObjectTypeDeterminer

Decides the key class, element class, key property and create-if-null behaviour of collections and maps; the default is DefaultObjectTypeDeterminer

org.apache.struts2.conversion.annotations.Conversion, TypeConversion, ConversionRule, ConversionType

The annotations and enums that express the conversion properties in Java

org.apache.struts2.interceptor.StrutsConversionErrorInterceptor

The interceptor conversionError of struts-default.xml

The Bookshelf Used on This Page

All examples below are classes and mappings of one WAR, g9typeconv.war, deployed under the context path /g9typeconv on Tomcat 11.0.26. It is the Bookshelf of the page Struts Getting Started (the Struts BOM org.apache.struts:struts2-bom:7.4.0 imported in dependencyManagement, struts2-core, the Servlet and Pages APIs with scope provided, StrutsPrepareAndExecuteFilter mapped to /*) with a few actions added for the experiments. Every action URL uses the default extension .action, every request in the listings was sent with curl using POST and --data-urlencode (the brackets and parentheses of indexed names are not legal in a raw request target on Tomcat 11, which answers 400 Invalid character found in the request target if they are sent unencoded in a GET URL), and the actions return the plain text of report() through a small JSP, so the listings show exactly what the action received. Every setter that takes a request parameter carries @StrutsParameter, which Struts 7 requires before a property can be set from the request (the page Parameter Injection and @StrutsParameter covers that annotation; for this page it is a prerequisite that explains the annotations visible in the listings). The package is org.example.bookshelf; imports are left out.

Table 2. The classes of the example WAR
Class Mapping Concept

BasicAction

basic

Fifteen properties of built-in types: int, Integer, long, boolean, Boolean, double, BigDecimal, BigInteger, char, an enum, Date, LocalDate, String[], int[], List<String>

CustomAction

custom

An Isbn (application-wide converter), a PageRange (action-level converter), a Book with a ShelfCode (bean-level converter), and a property whose converter throws an unexpected exception

EditAction

edit

A form with pages, published, listPrice and isbn that returns input on a conversion error, with a per-field message

CollectionAction

collection

A List<Book>, a Map<String,Book>, a Map<Long,String>, a Collection<Book> keyed by id, a Set<String> and a Book[]

NullAction

null

Null properties of a bean type and of an interface type

AnnotatedAction

annotated

@Conversion and @TypeConversion on a setter, on a field and on a collection

StrictAction

strict, lenient

The same two properties behind a stack that excludes conversionError from execute, and behind the default stack plus a conversion validator

Built-in Type Conversion

The Core Developer Guide lists what the framework converts without any configuration: String, boolean and Boolean, char and Character, the numeric primitives and their wrappers, dates (it says "the SHORT format for the Locale associated with the current request"), arrays, collections, enumerations, and BigDecimal and BigInteger. The list is correct as far as it goes; the sections below show what the converters actually accept, because the details decide whether a form works for a given visitor.

The test action has one property per family. Only the getter and the annotated setter of three of them are shown; the other twelve follow the same pattern.

public class BasicAction extends ActionSupport {
    private int pages;
    private Integer copies;
    private BigDecimal listPrice;
    private Format format;            // enum Format { PAPERBACK, HARDCOVER, EBOOK }
    private Date published;
    private LocalDate added;
    private String[] tags;
    private int[] ratings;
    private List<String> keywords;
    // ... id, available, featured, price, serial, grade ...

    @Override
    public String execute() { return SUCCESS; }

    public int getPages() { return pages; }
    @StrutsParameter public void setPages(int v) { pages = v; }
    public Format getFormat() { return format; }
    @StrutsParameter public void setFormat(Format v) { format = v; }
    public Date getPublished() { return published; }
    @StrutsParameter public void setPublished(Date v) { published = v; }
}

One request sets every property:

$ curl -sg -X POST http://localhost:18080/g9typeconv/basic.action \
    --data-urlencode pages=12 --data-urlencode copies=3 --data-urlencode id=9000000000 \
    --data-urlencode available=true --data-urlencode price=19.95 --data-urlencode listPrice=1234.50 \
    --data-urlencode serial=123456789012345678901234567890 --data-urlencode grade=ABC \
    --data-urlencode format=HARDCOVER --data-urlencode published=2026-10-02 --data-urlencode added=2026-10-02 \
    --data-urlencode tags=a --data-urlencode tags=b --data-urlencode ratings=1 --data-urlencode ratings=5 \
    --data-urlencode keywords=x,y --data-urlencode keywords=z

pages = 12 (Integer)
copies = 3 (Integer)
id = 9000000000 (Long)
available = true (Boolean)
featured = null (null)
price = 19.95 (Double)
listPrice = 1234.50 (BigDecimal)
serial = 123456789012345678901234567890 (BigInteger)
grade = A (Character)
format = HARDCOVER (Format)
published = Fri Oct 02 00:00:00 CEST 2026 (Date)
added = 2026-10-02 (LocalDate)
tags = [a, b] (ArrayList)
ratings = [1, 5] (String)
keywords = [x,y, z] (XWorkList)

fieldErrors = {}

Reading the output line by line shows the rules:

  • A single value goes to a scalar; several values with the same name go to an array or a collection (tags, ratings, keywords), and each element is converted individually.

  • A String is never split: keywords=x,y is one element, x,y. Splitting on commas is the job of the page, not of the framework.

  • char takes the first character of the string (ABC becomes A), and an empty string leaves the default.

  • Numbers keep their precision: BigInteger and BigDecimal have no 64-bit limit, and listPrice=1234.50 keeps the trailing zero.

  • An enum is matched by constant name and is case sensitive: format=HARDCOVER works, format=hardcover is a conversion error (observed).

  • The list for keywords is an XWorkList, a framework ArrayList subclass that supports the auto-growth of indexed names described in Collections and Maps.

  • A Date is parsed with the formats listed in Dates, and a LocalDate with the ISO form only.

Booleans, Checkboxes and Blank Values

Boolean conversion is Boolean.valueOf of the string: only the text true, in any letter case, is true (observed with Struts 7.4.0, where the converter’s doConvertToBoolean does exactly that). Everything else, including the on that browsers send for a checkbox without a value attribute, is false and is not an error:

available=on        -> available = false (Boolean)     fieldErrors = {}
featured=yes        -> featured = false (Boolean)
featured=1          -> featured = false (Boolean)
available=TRUE      -> available = true (Boolean)
available=true&available=false  -> available = true   (the first value of a repeated name wins for a scalar)
featured=           -> featured = false (Boolean)      (an empty string is not null for a Boolean)

The consequence for forms is the classic checkbox problem: an unchecked checkbox sends nothing, so the property is never touched and keeps its previous (possibly true) value. The checkbox interceptor of defaultStack handles it: a hidden companion field named __checkbox_featured sets the property to false when the checkbox itself is absent. With the companion field present and the checkbox absent, the observed result is featured = false; with both present and set to true, featured = true. The <s:checkbox> tag renders the companion field for you; the tags page of this section covers it.

Blank values need a separate rule, because a browser submits an empty text field as name=:

Table 3. What an empty string does to each family (observed on BasicAction)
Target Result of name=

int, long, double (primitives)

No error; the property keeps its default (0, 0.0)

Integer, Long, Double (wrappers)

null, no error

BigDecimal, BigInteger

null, no error

Boolean

false

char

No error; the property stays at the default character

String

The empty string

String[] with tags=&tags=z

[, z]: the empty element is kept

enum, Date, LocalDate

null, no error

The guide’s "Null and Blank Values" section says that "a blank string also cannot be set on a BigDecimal or BigInteger". That is not the behaviour of 7.4.0 (see Inconsistencies in the Official Documentation). The other half of the advice stands: a primitive cannot be null, so an action that must tell "not entered" from "zero" declares the wrapper type and uses validation to demand a value.

Numbers and the Request Locale

NumberConverter converts with java.text.NumberFormat for the locale of the request: the locale chosen by the i18n interceptor (from request_locale, the session or the browser’s Accept-Language). That makes the decimal and grouping separators locale dependent, in two different ways for two kinds of target:

# JVM and request locale en-US
copies=1,234        -> copies = 1234 (Integer)               grouping separator accepted for a wrapper
pages=1,234         -> fieldErrors = {pages=[...]}            the primitive int does not accept it
copies=12.7         -> fieldErrors = {copies=[...]}           Integer: "Unparseable number ... at position 2"
copies= 5           -> fieldErrors = {copies=[...]}           leading space
copies=99999999999  -> fieldErrors = {copies=[...]}           overflow is an error, not a wrap-around
price=1,5           -> price = 15.0 (Double)                  "," is the grouping separator in en-US: silently ignored
listPrice=1,5       -> listPrice = 15 (BigDecimal)
price=1e3           -> fieldErrors = {price=[...]}            exponent notation is rejected
id=7L               -> fieldErrors = {id=[...]}

# the same form with Accept-Language: de-DE
price=1,5           -> price = 1.5 (Double)
listPrice=1.234,5   -> listPrice = 1234.5 (BigDecimal)

Three things to remember. A decimal comma typed by a visitor whose locale is en-US is not an error: it is dropped as a grouping separator and 1,5 becomes fifteen. The primitive int and long go through the plain DefaultTypeConverter (no grouping), whereas the wrapper types and BigDecimal use NumberFormat. And the locale is the one the framework resolved, which is why a form and its action must agree on how the locale is chosen (the page Localization and i18n covers the i18n interceptor). For prices the safest design is a BigDecimal property, an explicit NumberFormat-aware validation rule, and the same locale for rendering and parsing.

Dates

The Core Developer Guide says dates use "the SHORT format for the Locale associated with the current request". In 7.4.0 DateConverter is more generous: for a java.util.Date property it tries a list of DateFormat instances in order and uses the first that parses. If a global format is defined through the struts.date.format text key it goes first; then the formats are:

Order Format

1

SHORT date and LONG time for the locale

2

SHORT date and MEDIUM time for the locale

3

SHORT date and SHORT time for the locale

4

yyyy-MM-dd'T'HH:mm:ss

5

SHORT date for the locale

6

MEDIUM date for the locale

7

LONG date for the locale

8

yyyy-MM-dd

The results on published (a java.util.Date, JVM locale en-US, time zone Central Europe):

published=2026-10-02            -> Fri Oct 02 00:00:00 CEST 2026     format 8
published=2026-10-02T10:15:30   -> Fri Oct 02 10:15:30 CEST 2026     format 4
published=10/02/2026            -> Fri Oct 02 00:00:00 CEST 2026     SHORT, month first in en-US
published=Oct 2, 2026           -> Fri Oct 02 00:00:00 CEST 2026     MEDIUM
published=2026-10-02 10:15:30   -> Fri Oct 02 00:00:00 CEST 2026     time lost: format 8 stops reading after the date
published=02.10.2026            -> fieldErrors = {published=[...]}   not an en-US format
published=2026/10/02            -> fieldErrors = {published=[...]}
published=Friday, October 2, 2026 -> fieldErrors = {published=[...]}  FULL is not in the list
published=2026-13-45            -> fieldErrors = {published=[...]}   non-lenient parsing for the final fallback

Two behaviours deserve a warning. The year-first yyyy-MM-dd format reads only the leading part of the text, so 2026-10-02 10:15:30 (with a space) silently becomes midnight, while the T form keeps its time. And the result of a numeric short date depends on the locale of the request, as the same value 02/10/2026 is 2 October for a visitor on en-GB and 10 February for en-US (the SHORT pattern decides which field is the month). For forms that matter, define one format and use it for input and output: a text key struts.date.format in the global resource bundle puts that format first, or a custom converter (see below) owns the format completely.

The java.time types are handled separately: LocalDateTime, LocalDate and LocalTime are parsed with ISO_LOCAL_DATE_TIME, ISO_LOCAL_DATE and ISO_LOCAL_TIME (preceded by the global format when one is defined), and OffsetDateTime with ISO_OFFSET_DATE_TIME. That is why added=2026-10-02 works and added=02/10/2026 fails with a conversion error (observed). The guide does not mention java.time at all.

Arrays, Collections and String Properties

For an array or a collection property the converter converts each submitted value to the element type and builds the container: an array of the property’s type, a HashSet for Set, a TreeSet for SortedSet and an XWorkList (a framework ArrayList subclass) for List and Collection (read from CollectionConverter in the 7.4.0 source). When the element type cannot be determined the guide says elements are treated as String. The Set<String> genres of CollectionAction collapses duplicates:

genres=a  genres=b  genres=a   ->   genres = [a, b]
ratings=1  ratings=x           ->   ratings stays null; fieldErrors = {ratings=[...]}   (one bad element rejects the array)

A property of type String receives the raw parameter; an Object or Number property that already holds a compatible value is left alone. The framework also converts to Class (by Class.forName) and to Calendar; both are rarely useful for request input and Class conversion from request text is a reason to keep the allowlist of the OGNL layer switched on (the page Security Hardening covers it).

Parameter Names, OGNL and Null Property Handling

The guide gives the central rule in one sentence: there is no need to capture form values in intermediate strings and primitives; name the inputs with OGNL expressions and the framework reads and writes the properties of the objects they address, converting on the way. person.name means getPerson().setName(...). The consequences, each verified on the Bookshelf:

  • The target of a dotted name is created when it is null. On NullAction the property review is declared Review review; (never assigned), and review.text=hi produces a Review (the framework calls the no-argument constructor and setReview), then sets the text. A setReview method must exist, and so must the getter used for the lookup.

  • An object that already exists is never replaced. ready is initialised in the field declaration and ready.stars=4 sets a property on the existing instance.

  • An interface cannot be created. rating is declared as Rateable (an interface) and is null, so rating.stars=3 is silently discarded.

public class NullAction extends ActionSupport {
    private Review review;                    // class with a public no-arg constructor
    private Rateable rating;                  // interface
    private Review ready = new Review();      // already created

    @StrutsParameter(depth = 1) public Review getReview() { return review; }
    public void setReview(Review v) { review = v; }
    @StrutsParameter(depth = 1) public Rateable getRating() { return rating; }
    public void setRating(Rateable v) { rating = v; }
    @StrutsParameter(depth = 1) public Review getReady() { return ready; }
    public void setReady(Review v) { ready = v; }
    // report() and execute() omitted
}
$ curl -sg -X POST .../g9typeconv/null.action --data-urlencode review.text=hi \
      --data-urlencode rating.stars=3 --data-urlencode ready.stars=4

review = Review{stars=0, text=hi}
rating = null
ready = Review{stars=4, text=null}

The guide spells out the rules: a property declared exactly as Collection or List gets an ArrayList, a Map gets a HashMap, and a bean with a no-argument constructor is built through the ObjectFactory. Null creation is switched on only while the params interceptor runs (the context key CREATE_NULL_OBJECTS), so reading review in an action method does not create one. The @StrutsParameter(depth = 1) on the getters is what permits the request to reach one level below review: the depth rules belong to the page Parameter Injection and @StrutsParameter. A conversion error inside the nested object is reported with the full dotted name, review.stars (observed: review.stars=x gives the field error review.stars).

The guide recommends creating the target objects in the constructor or in a prepare() method when they should not be built on demand, and notes that the framework "will not instantiate an object if an instance already exists". It also gives the naming advice for lists and maps: use index notation (people[0].name, friends['patrick'].name), and for a multiple select box name the element people.name so that the framework creates one Person per selected item.

Creating a Custom Type Converter

When the built-in rules are not right, or the target is a type Struts has never heard of, an application writes a converter. The guide’s recipe is to extend org.apache.struts2.util.StrutsTypeConverter: two methods, one for each direction. The Bookshelf has an Isbn value type (a thirteen digit string with a canonical hyphenated form):

public class Isbn {
    private final String digits;
    public Isbn(String digits) { this.digits = digits; }
    public String getDigits() { return digits; }
    @Override public String toString() { return "Isbn[" + digits + "]"; }
}

public class IsbnConverter extends StrutsTypeConverter {

    @Override
    public Object convertFromString(Map context, String[] values, Class toClass) {
        String text = values[0].replace("-", "").trim();
        if (text.length() != 13 || !text.chars().allMatch(Character::isDigit)) {
            throw new TypeConversionException("not an ISBN-13: " + values[0]);
        }
        return new Isbn(text);
    }

    @Override
    public String convertToString(Map context, Object o) {
        String d = ((Isbn) o).getDigits();
        return d.substring(0, 3) + "-" + d.substring(3, 4) + "-" + d.substring(4, 6) + "-"
                + d.substring(6, 12) + "-" + d.substring(12);
    }
}

The points of the contract:

  • convertFromString(Map context, String[] values, Class toClass) receives all the submitted values of the parameter as an array (a repeated name gives several), and toClass is the declared type of the property. A converter that serves one type usually reads values[0].

  • convertToString(Map context, Object o) is called when a tag or a result needs the property as text, for example <s:property value="isbn"/> or the value of a text field. Without it, the text of an Isbn would be its toString().

  • StrutsTypeConverter extends DefaultTypeConverter, which implements the TypeConverter interface of the framework. The base class also offers the hook performFallbackConversion, which the guide describes as a fall-back for when the conversion of the converter fails; by default it delegates to the superclass conversion. A converter that does not need the two-way contract can implement TypeConverter directly, or extend DefaultTypeConverter and override convertValue, and signal "not my type" with TypeConverter.NO_CONVERSION_POSSIBLE.

  • To report a bad value, throw TypeConversionException (or, as the guide puts it, XWorkException). The message is for the log, not for the visitor: the visitor sees the text of the xwork.default.invalid.fieldvalue key (see Conversion Errors).

  • The context map is the OGNL context; DefaultTypeConverter offers the protected method getLocale(context), which returns the locale of the current ActionContext (and falls back to the configured locale provider), so a converter can follow the request locale like the built-in ones.

  • The converter should be stateless and thread safe, because the framework may reuse one instance for every request.

The same converter on a model that contains a second kind of value, a page range 10-20, shows the pattern for a value that spans several fields of the target:

public record PageRange(int from, int to) {
    public int getFrom() { return from; }
    public int getTo() { return to; }
    @Override public String toString() { return from + "-" + to; }
}

public class PageRangeConverter extends StrutsTypeConverter {
    @Override
    public Object convertFromString(Map context, String[] values, Class toClass) {
        String[] parts = values[0].split("-");
        try {
            if (parts.length != 2) {
                throw new TypeConversionException("expected from-to");
            }
            return new PageRange(Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()));
        } catch (NumberFormatException e) {
            throw new TypeConversionException(e.getMessage());
        }
    }

    @Override
    public String convertToString(Map context, Object o) { return o.toString(); }
}

Writing the converter is half of the job; the other half is telling Struts when to use it. There are three scopes, and each is a properties file or an annotation.

Applying a Converter: Action, Bean and Application

For one Action

A file named ActionClassName-conversion.properties in the same classpath directory as the action class maps a property name to a converter class. The Bookshelf’s CustomAction has a range property:

# src/main/resources/org/example/bookshelf/CustomAction-conversion.properties
# syntax: <propertyName> = <converterClassName>
range=org.example.bookshelf.PageRangeConverter

The file is found by the class name of the action, so it belongs in the resources directory of the same package as the class (org/example/bookshelf/ here). The key can be a nested property, person.phoneNumber=com.acme.PhoneNumberConverter.

For a Bean or Model

When the property belongs to a bean (a model object, a class in a dependency that cannot be modified), the rule file is named after the bean class and sits next to it on the classpath: ClassName-conversion.properties. The guide’s example is a Measurement class from a dependency, used through ModelDriven<Measurement>, so that the action cannot hold the rule: create the same package directory in your own resources and put the file there. In the Bookshelf the Book has a shelf property of type ShelfCode:

# src/main/resources/org/example/bookshelf/Book-conversion.properties
shelf=org.example.bookshelf.ShelfCodeConverter

For the Whole Application

The key of an application-wide rule is a type, not a property: the rule applies to every property of that type in every action and bean. The file is struts-conversion.properties in the root of the classpath (WEB-INF/classes); the old name xwork-conversion.properties is still read but is marked deprecated in the guide.

# src/main/resources/struts-conversion.properties
# syntax: <type> = <converterClassName>
org.example.bookshelf.Isbn=org.example.bookshelf.IsbnConverter

The guide adds that since Struts 7.2.0, when the Spring plugin is active, the converter value can be the name of a Spring bean (java.math.BigDecimal = myBigDecimalConverter) instead of a class name, so a converter can use injected services. That sentence is taken from the guide and was not run on the scratch WAR, which has no Spring plugin. The core jar itself ships one application-wide rule in struts-default-conversion.properties: java.io.File is converted by org.apache.struts2.conversion.UploadedFileConverter, which is how uploaded files reach File properties (the file upload page of this section covers it).

The three scopes together, with every converter used and one request:

public class CustomAction extends ActionSupport {
    private Isbn isbn;          // struts-conversion.properties: Isbn -> IsbnConverter
    private PageRange range;    // CustomAction-conversion.properties: range -> PageRangeConverter
    private Book book;          // Book-conversion.properties: shelf -> ShelfCodeConverter (and isbn by the type rule)

    public Isbn getIsbn() { return isbn; }
    @StrutsParameter public void setIsbn(Isbn v) { isbn = v; }
    public PageRange getRange() { return range; }
    @StrutsParameter public void setRange(PageRange v) { range = v; }
    @StrutsParameter(depth = 1) public Book getBook() { return book; }
    public void setBook(Book v) { book = v; }
    // report() returns the three properties as text; execute() returns SUCCESS
}
$ curl -sg -X POST .../g9typeconv/custom.action --data-urlencode isbn=978-0-13-468599-1 \
      --data-urlencode range=10-20 --data-urlencode book.shelf=B-7 --data-urlencode book.isbn=9780134685991 \
      --data-urlencode book.pages=300 --data-urlencode book.title=Dune

isbn = Isbn[9780134685991]
range = 10-20
book = Book{id=null, title=Dune, pages=300, shelf=B-7, isbn=Isbn[9780134685991]}

fieldErrors = {}

$ curl -sg -X POST .../g9typeconv/custom.action --data-urlencode isbn=12 \
      --data-urlencode range=x --data-urlencode book.shelf=zz

isbn = null
range = null
book = Book{id=null, title=null, pages=null, shelf=null, isbn=null}

fieldErrors = {isbn=[Bookshelf: invalid value for isbn], range=[Bookshelf: invalid value for range], book.shelf=[Bookshelf: invalid value for book.shelf]}

Note the three details in the second response: all three conversions failed, so all three properties stayed null; the nested failure is named book.shelf; and the Book object was created (it is printed with its fields null), because creating the target is separate from converting its values. The message Bookshelf: invalid value for isbn is the xwork.default.invalid.fieldvalue key overridden in the application’s bundle (see below).

Any Exception Is a Conversion Error

The guide says a converter throws TypeConversionException (or XWorkException) "to allow Struts to recognize that a conversion error has occurred". The scratch app also has a converter that throws a plain IllegalStateException, wired to a boom property through CustomAction-conversion.properties; the request is answered like any other conversion error and nothing leaks to the container:

$ curl -sg -X POST .../g9typeconv/custom.action --data-urlencode boom=1-2 --data-urlencode range=1-2

isbn = null
range = 1-2
book = null
boom = null

fieldErrors = {boom=[Bookshelf: invalid value for boom]}

So the safe reading of the contract is that a converter signals a bad value by throwing, and that the framework treats any RuntimeException the same way, but a converter that wants a clean log and a precise exception type should throw TypeConversionException (or its subclasses) itself.

Collections and Maps

A form often edits several objects at once: the three books of a wish list, a table of notes keyed by book id. Struts supports this through the index notation of OGNL names and through the Element_, Key_, KeyProperty_ and CreateIfNull_ rules of a -conversion.properties file. The ObjectTypeDeterminer reads those rules (the default implementation is DefaultObjectTypeDeterminer; a custom one can be registered), and where the property is declared with generics the determiner reads the type argument.

Index Notation With Generics

With generics the rule files are not needed at all, because the type arguments say what to build. CollectionAction is the test bed:

public class CollectionAction extends ActionSupport {
    private List<Book> books = new ArrayList<>();
    private Map<String, Book> shelves = new HashMap<>();
    private Map<Long, String> notes = new TreeMap<>();
    private Collection<Book> catalogue = new ArrayList<>();
    private Set<String> genres;
    private Book[] picks = new Book[3];

    @StrutsParameter(depth = 2) public List<Book> getBooks() { return books; }
    public void setBooks(List<Book> v) { books = v; }
    @StrutsParameter(depth = 2) public Map<String, Book> getShelves() { return shelves; }
    public void setShelves(Map<String, Book> v) { shelves = v; }
    @StrutsParameter(depth = 1) public Map<Long, String> getNotes() { return notes; }
    public void setNotes(Map<Long, String> v) { notes = v; }
    // catalogue, genres and picks follow; execute() returns SUCCESS
}
$ curl -sg -X POST .../g9typeconv/collection.action \
    --data-urlencode "books[0].title=A" --data-urlencode "books[1].pages=5" \
    --data-urlencode "shelves['top'].title=T" --data-urlencode "shelves.low.title=L" \
    --data-urlencode "notes[7]=seven" --data-urlencode "genres=a" --data-urlencode "genres=b" --data-urlencode "genres=a"

books = [Book{id=null, title=A, ...}, Book{id=null, title=null, pages=5, ...}]
shelves = {top=Book{id=null, title=T, ...}, low=Book{id=null, title=L, ...}}
notes = {7=seven}
genres = [a, b]

What the output shows. A List<Book> is addressed with books[0], books[1], and the missing elements are created and converted from the generic argument. A Map<String,Book> accepts both shelves['top'] and the shorter shelves.low; the key 7 of Map<Long,String> was converted to a Long by the same mechanism. A Set has no index; repeated values are collapsed. If a list index skips ahead the gaps are filled with null: submitting only books[255].title=ok yields a list of 256 elements of which the first 255 are null (observed, see the growth limit below), so templates must tolerate null elements. Elements that failed to convert are reported by their full name: books[0].pages=zz gives the field error books[0].pages.

Element_, Key_, KeyProperty_ and CreateIfNull_

Where generics are not available, or where the collection is addressed by something other than its position, the rules go into ActionClassName-conversion.properties (or the conversion file of whatever bean holds the property):

Table 4. The collection rules of a conversion properties file
Rule Meaning

Element_<property>=<class>

The class of the elements of a Collection, List, Set or the values of a Map (replaces the older Collection_<property>, still read but deprecated)

Key_<property>=<class>

The class of the keys of a Map

KeyProperty_<property>=<name>

The property of the element that identifies it, so that an element is addressed by that property’s value with the () notation instead of by position

CreateIfNull_<property>=true

Create elements, and the collection itself, when they are missing (the default is true for lists and maps)

The Bookshelf uses catalogue, a Collection<Book> that is edited by book id:

# src/main/resources/org/example/bookshelf/CollectionAction-conversion.properties
KeyProperty_catalogue=id
Element_catalogue=org.example.bookshelf.Book
CreateIfNull_catalogue=true
$ curl -sg -X POST .../g9typeconv/collection.action \
    --data-urlencode "catalogue(5).title=Five" --data-urlencode "catalogue(5).id=5" --data-urlencode "catalogue(6).title=Six"

catalogue = [Book{id=5, title=Five, ...}, Book{id=6, title=Six, ...}]

The () notation (parentheses, not brackets: "Notice the () notation! Do not use [] notation, which is for Maps only", in the words of the guide’s example) says "the element whose id is 5". The value in the parentheses is converted to the type of the key property (Long here), and the element is looked up, or created when CreateIfNull is on, with the key property set to it. The book with id=6 came into existence although only its title was submitted, because the key in the name set its id. The guide stresses the practical gain: a row of a table stays tied to its identifier, so a form can edit items in any order, or after another user deleted one, without the index shift that books[2] suffers. The page of the guide uses an iterator:

<s:form action="collection" theme="simple">
<s:iterator value="catalogue" var="bean">
  <s:textfield name="%{'catalogue(' + #bean.id + ').title'}" value="%{#bean.title}"/>
</s:iterator>
</s:form>

Rendered by the scratch app for the request that set books 5 and 6, the page contains one input per element, named by id:

<input type="text" name="catalogue(5).title" value="Five" id="collection_catalogue_5__title"/>
<input type="text" name="catalogue(6).title" value="Six" id="collection_catalogue_6__title"/>

The guide’s version of that listing writes id="bean", <stextfield and the name beanList(%{bean.id}).name; the typo and the retired id attribute (replaced by var since 2.1.x) are listed under the inconsistencies.

makeNew, the Creation of Elements and the Growth Limit

An element addressed by key is not created when it does not exist, unless CreateIfNull is on: fooCollection(22) that is missing is simply absent, "to avoid OutOfMemoryErrors". For adding new elements the guide offers the notation fooCollection.makeNew[0], fooCollection.makeNew[1]. On the Bookshelf:

$ curl -sg -X POST .../g9typeconv/collection.action \
    --data-urlencode "catalogue.makeNew[0].title=Phil" --data-urlencode "catalogue.makeNew[1].title=John"

catalogue = [Book{id=null, title=Phil, ...}, Book{id=null, title=John, ...}]

The guide’s caveat applies to a Set: equals and hashCode of the element must not rest only on the id, or two new elements, both with a null id, collapse into one.

An index is an invitation to allocate memory, so the growth is limited. The constant struts.ognl.autoGrowthCollectionLimit (it replaced the deprecated xwork.autoGrowCollectionLimit) caps the highest index an indexed name can create. It is not set in the default properties file in 7.4.0; the documented default is 255, which means that 256 elements are allowed. The behaviour on the Bookshelf agrees with that:

books[255].title=ok      -> books has 256 elements (255 nulls, then the book)
books[256].title=nope    -> books = []     (the parameter is dropped; no field error, no exception)
books[300].title=nope    -> books = []

The limit exists to prevent a denial of service by a parameter named books[2000000000].title. A form that really needs more rows raises it in struts.xml: <constant name="struts.ognl.autoGrowthCollectionLimit" value="1024"/>. Note that going over the limit is silent: the data is lost without a conversion error or a log line at the default level, so an unexpectedly empty list is worth checking against this limit.

What Does Not Convert

  • A map whose key cannot be converted does not report an error: notes[x]=bad with Map<Long,String> leaves notes empty (observed; compare books[0].pages=zz, which is reported).

  • An array of beans does not grow: picks[1].title=P on a pre-sized Book[3] left every element null (observed with @StrutsParameter(depth = 2) on the getter). Use a List<Book> for repeated beans.

  • The elements of a raw List are String when no Element_ rule is present (the guide: "it is assumed to be a String").

  • A property declared with a custom interface cannot be instantiated (the rating example above); initialise the field or create the object in a prepare() method.

  • Generics lost to erasure break conversion in abstract hierarchies. The guide’s example is Measurement<T extends Enum> with a setter setUnits(T): the signature after erasure is setUnits(java.lang.Enum), the framework tries to instantiate Enum and fails with IllegalArgumentException: java.lang.Enum is not an enum type. The fix is to work around the erasure with a property of the concrete type. The guide’s example was not run here; the rule it states (that the converter sees only the erased parameter type) is the one the numbers above follow.

Conversion Annotations

Everything a -conversion.properties file says can be written as annotations on the action. The class carries @Conversion; each rule is a @TypeConversion, on a method, and since Struts 7.3.0 also on a field.

Table 5. The annotation attributes (read from the 7.4.0 jar and the Core Developer Guide)
Attribute Default Meaning

@Conversion(conversions = {...})

none

Marker at the type level; the optional array holds rules that apply to the class (keys must then name the property)

@TypeConversion(key = ...)

empty (the annotated property’s name is used)

The property (for ConversionType.CLASS) or the fully qualified class name (for ConversionType.APPLICATION)

type

ConversionType.CLASS

CLASS writes the rule as if it were in ActionClassName-conversion.properties; APPLICATION writes it as if it were in struts-conversion.properties

rule

ConversionRule.PROPERTY

One of PROPERTY, ELEMENT (since 7.3.0 the preferred one), COLLECTION (deprecated), MAP, KEY, KEY_PROPERTY, CREATE_IF_NULL

converterClass

XWorkBasicConverter

The converter class; for an ELEMENT or KEY rule, the class of elements or keys

converter

empty

The converter class name as a string; marked deprecated in the guide in favour of converterClass

value

empty

The value of a KEY_PROPERTY or CREATE_IF_NULL rule

The Bookshelf’s AnnotatedAction uses the three placements: a setter with a string converter name, a field, and a list element rule.

@Conversion
public class AnnotatedAction extends ActionSupport {
    private PageRange range;

    @TypeConversion(converterClass = ShelfCodeConverter.class)     // on a field: since 7.3.0
    private ShelfCode shelf;

    private List<Book> wishlist = new ArrayList<>();

    public PageRange getRange() { return range; }
    @StrutsParameter
    @TypeConversion(converter = "org.example.bookshelf.PageRangeConverter")   // on a setter, string name
    public void setRange(PageRange v) { range = v; }

    public ShelfCode getShelf() { return shelf; }
    @StrutsParameter public void setShelf(ShelfCode v) { shelf = v; }

    @StrutsParameter(depth = 2)
    @TypeConversion(rule = ConversionRule.ELEMENT, converterClass = Book.class)   // elements of the list
    public List<Book> getWishlist() { return wishlist; }
    public void setWishlist(List<Book> v) { wishlist = v; }
    // report() and execute() omitted
}
$ curl -sg -X POST .../g9typeconv/annotated.action --data-urlencode range=3-9 --data-urlencode shelf=C-2 \
      --data-urlencode "wishlist[0].title=X" --data-urlencode "wishlist[1].pages=7"

range = 3-9
shelf = C-2
wishlist = [Book{id=null, title=X, ...}, Book{id=null, title=null, pages=7, ...}]

fieldErrors = {}

$ curl -sg -X POST .../g9typeconv/annotated.action --data-urlencode range=q --data-urlencode shelf=Z

range = null
shelf = null
wishlist = []

fieldErrors = {range=[Bookshelf: invalid value for range], shelf=[Bookshelf: invalid value for shelf]}

Three details of the annotations, taken from the Struts 7.3.0 changes and the guide:

  • Key derivation. Until 7.3.0 the rule prefix was added automatically only for annotations on methods; a class-level @Conversion(conversions = ...) had to write key = "CreateIfNull_users". From 7.3.0 the key is the bare property name for every rule and the prefix (CreateIfNull_, Element_, Key_, KeyProperty_) is derived. A key that already carries a known prefix is used as is and is never prefixed twice; the keys of ConversionType.APPLICATION rules are class names and are never prefixed.

  • Field placement. Since 7.3.0 @TypeConversion can be placed on a field, as shelf above, which keeps the rule next to the declaration.

  • COLLECTION is deprecated. ConversionRule.COLLECTION and the Collection_ prefix are deprecated (WW-5656); use ELEMENT, which "additionally covers the values of a Map". The engine still reads Collection_xxx as a fallback.

Conversion Errors

A conversion error does not stop a request; it leaves a trail. This section follows that trail from the converter to the form, with the Bookshelf’s EditAction behind a form of four fields.

The conversionErrors Map and the conversionError Interceptor

Each failed conversion is stored in the map conversionErrors of the ActionContext, keyed by the property name, with the rejected value and the target class. Nothing is reported yet. The interceptor conversionError (class StrutsConversionErrorInterceptor in struts-default.xml, a subclass of ConversionErrorInterceptor) reads the map after params has run and, for each entry, if the action is ValidationAware:

  • adds a field error with the text of the key xwork.default.invalid.fieldvalue (the default text of 7.4.0 is Invalid field value for field "{0}".) or the per-field override invalid.fieldvalue.<field>;

  • remembers the original value, so that any later read of the property from a tag returns what the visitor typed, not the value in the action (abc stays abc, not 0);

  • does nothing for a value that is null, the empty string or an array holding only the empty string, which is the variant that the Struts subclass adds so that an empty text field is not reported as an error.

The interceptor has no parameters of its own, extends MethodFilterInterceptor (so includeMethods and excludeMethods work) and is part of basicStack, defaultStack and paramsPrepareParamsStack. After it, validation can add more errors, and workflow returns input if there are any.

A Form That Keeps What the Visitor Typed

The edit mapping returns input to edit.jsp when conversion fails. The action has four properties with conversion rules (a primitive int, a Date, a BigDecimal and the Isbn of the application-wide converter), and two message keys: the global bundle global.properties, registered with the constant struts.custom.i18n.resources, overrides the generic message, and EditAction.properties overrides one field:

# src/main/resources/global.properties
xwork.default.invalid.fieldvalue=Bookshelf: invalid value for {0}

# src/main/resources/org/example/bookshelf/EditAction.properties
invalid.fieldvalue.pages=Pages must be a whole number.
<constant name="struts.custom.i18n.resources" value="global"/>
<package name="default" extends="struts-default" namespace="/">
    <action name="edit" class="org.example.bookshelf.EditAction">
        <result>/WEB-INF/views/report.jsp</result>
        <result name="input">/WEB-INF/views/edit.jsp</result>
    </action>
</package>
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:fielderror/>
<s:form action="edit" theme="simple">
  <s:textfield name="pages"/>
  <s:textfield name="published"/>
  <s:textfield name="listPrice"/>
  <s:textfield name="isbn"/>
  <s:submit/>
</s:form>

Submitting four invalid values returns the form again, with the errors and with the invalid text in the inputs (the output of curl for pages=abc published=notadate listPrice=1.5x isbn=bad, whitespace shortened):

<ul class="errorMessage">
    <li><span>Pages must be a whole number.</span></li>
    <li><span>Bookshelf: invalid value for isbn</span></li>
    <li><span>Bookshelf: invalid value for published</span></li>
    <li><span>Bookshelf: invalid value for listPrice</span></li>
</ul>
<form id="edit" name="edit" action="/g9typeconv/edit.action" method="post">
  <input type="text" name="pages" value="abc" id="edit_pages"/>
  <input type="text" name="published" value="notadate" id="edit_published"/>
  <input type="text" name="listPrice" value="1.5x" id="edit_listPrice"/>
  <input type="text" name="isbn" value="bad" id="edit_isbn"/>
  <input type="submit" value="Submit" id="edit_0"/>
</form>

Four things to read from it. The override invalid.fieldvalue.pages won over the global message for that one field. The original texts abc, notadate and 1.5x are back in the inputs, although the properties themselves are 0 and null. The order of the messages is not the order of the fields (the errors come from a map), so a page must not depend on it. And the escaping is the standard one of the tags: the interceptor escapes the original value for OGNL (escapeJava), and the s:textfield tag escapes it for HTML.

The per-field message may use the message formatting of the localization system, with the field label as {0}. Both the generic key and the per-field key go in the usual resource bundles of the action (the page Localization and i18n covers the search order), so a different language is a matter of adding global_es.properties with the same keys.

Keeping Errors Out: excludeMethods and the Validator Route

The guide: "To keep conversion errors from reporting globally, change the interceptor stack, and add additional validation rules." The two routes are checked on StrictAction, which has an Integer pages and an int copies, mapped twice.

The first mapping, strict, uses the default stack but excludes execute from conversionError:

<action name="strict" class="org.example.bookshelf.StrictAction">
    <interceptor-ref name="defaultStack">
        <param name="conversionError.excludeMethods">execute</param>
    </interceptor-ref>
    <result>/WEB-INF/views/report.jsp</result>
    <result name="input">/WEB-INF/views/report.jsp</result>
</action>
$ curl -sg -X POST .../g9typeconv/strict.action --data-urlencode pages=abc --data-urlencode copies=zz

pages = null
copies = 0

fieldErrors = {}

No error is raised, the invalid input is silently replaced by the default, and the action method runs with null and 0. That is the choice that "conversion errors are not an error" implies, and it is almost never what a form wants; the case for it is an action that does its own checking. The second mapping, lenient, keeps the default stack and adds a rule file with a conversion validator for one field:

<!-- StrictAction-lenient-validation.xml -->
<validators>
    <field name="pages">
        <field-validator type="conversion">
            <message>Pages: a whole number is required</message>
        </field-validator>
    </field>
</validators>
$ curl -sg -X POST .../g9typeconv/lenient.action --data-urlencode pages=abc --data-urlencode copies=zz

fieldErrors = {copies=[Bookshelf: invalid value for copies],
               pages=[Bookshelf: invalid value for pages, Pages: a whole number is required]}

With both the interceptor and the validator the field gets two messages: the generic one from conversionError and the validator’s own. The Struts constant struts.validators.skipValidatorsOnConversionError (default false in 7.4.0; the Version Notes of 7.3.0 list the ticket WW-2934, "conversion error should short-circuit all validators") skips the other validators of a field that failed to convert, to avoid a second message of a "required" or "range" rule for a value that never became a number; as the comment of default.properties says, the field’s own conversion validator still runs. The validator also takes the parameter repopulateField: set to true, it re-fills the stack with the original value for a form that does not use the conversionError interceptor. The validation page of this section describes the validator framework itself.

What a Conversion Error Does Without an input Result

Because workflow returns input when there are field errors, a mapping without an input result (or with the action’s execution still reachable) fails in the way the page Exception Handling and Global Results shows: with a global java.lang.Exception mapping the failure to find the input result is swallowed into the polite error page with HTTP 200, which is a confusing way to learn that an id=abc was typed. Map input for every action that receives request parameters (see Exception Handling and Global Results).

Common Problems at a Glance

Table 6. Type conversion pitfalls: symptom, cause and fix (rows marked "observed" were reproduced on Tomcat 11.0.26 with Struts 7.4.0)
Symptom Cause Fix

A checkbox or "yes" value arrives as false, with no error (observed)

Boolean conversion is Boolean.valueOf: only true is true

Give the checkbox value="true" and keep the __checkbox_name companion field; validate the text with a converter if "yes" must work

A price 1,5 becomes 15.0 for an English visitor (observed)

The comma is the grouping separator of en-US and is ignored

Decide the locale of input; use a validator for a pattern, or a custom converter with a strict DecimalFormat

pages=1,234 fails for an int and works for an Integer (observed)

Primitives use the plain converter without grouping; wrappers use NumberFormat

Use one family consistently

A date typed as 02.10.2026 is rejected, 2026-10-02 10:15:30 loses its time (observed)

The DateConverter format list; the yyyy-MM-dd format parses a prefix and ignores the rest

Define struts.date.format or write a converter; use ISO with T

The same date means two days to two visitors

SHORT dates follow the locale of the request

Do not accept ambiguous numeric dates; use a date picker that sends ISO

A list stays empty after a large form (observed)

struts.ognl.autoGrowthCollectionLimit (255 by default) drops higher indexes silently

Raise the constant, or paginate the form

A Map<Long,...> loses an entry whose key is not a number (observed)

A key that cannot be converted is dropped without an error

Validate keys; use String keys

An array of beans does not fill (observed)

Array elements are not created from indexed names

Use List<Book>

rating.stars=3 has no effect (observed)

An interface property cannot be instantiated

Initialise it, or create it in prepare()

conversionError shows nothing for an empty field (observed)

The Struts variant ignores null, "" and {""}

Use a required rule in validation

A bad value is replaced by 0 or null and the action runs (observed)

The action’s stack excludes conversionError, or the action is not ValidationAware

Keep conversionError in the stack; extend ActionSupport; map input

Two messages for one field (observed)

conversionError and a conversion validator both report

Use one of them, or set struts.validators.skipValidatorsOnConversionError

A custom converter has no effect

The conversion properties file is in the wrong package, has the wrong name (Action-conversion.properties, not Action.conversion.properties) or maps a different property name

Put it in the class’s package, directly under src/main/resources

A converter is called for a type it was not written for

An application-wide rule names a type, so it applies everywhere

Narrow it to a property rule, and check toClass in the converter

The application cannot assign a property although a converter exists

No @StrutsParameter on the setter (or insufficient depth for nested names)

See the parameter injection page; this is not a conversion problem

A converter throws a plain exception and the log is noisy (observed: the visitor still sees only a field error)

Any RuntimeException is recorded as a conversion error, not only TypeConversionException

Throw TypeConversionException with a message that names the bad text

What Changed Recently

Table 7. Type conversion changes by Struts version (from the Version Notes, the guide and the 7.4.0 jar)
Version Change

2.5.2

conversionError extends MethodFilterInterceptor (includeMethods and excludeMethods apply)

7.0.0

All com.opensymphony.xwork classes move to org.apache.struts2: StrutsTypeConverter is in org.apache.struts2.util, the converter classes in org.apache.struts2.conversion and conversion.impl, the interceptors in org.apache.struts2.interceptor

7.0.0 (the @StrutsParameter requirement)

Properties can be set from the request only through annotated setters; the examples of older books without the annotation do nothing

7.2.0

With the Spring plugin, struts-conversion.properties accepts Spring bean names as converter values (guide)

7.3.0

@TypeConversion derives the rule prefix and can be placed on fields (WW-3871); ConversionRule.COLLECTION and Collection_ are deprecated, use ELEMENT (WW-5656); a conversion error can short-circuit the other validators of the field (WW-2934, constant struts.validators.skipValidatorsOnConversionError)

before 7.4.0 (version not stated in the guide)

The growth limit constant is struts.ognl.autoGrowthCollectionLimit; the old name xwork.autoGrowCollectionLimit is deprecated

Where the Books Are Outdated

The books of the section teach the idea correctly: request parameters named with OGNL, converters registered by properties files, conversion errors collected and shown beside the field. The details have moved.

Table 8. Type conversion in the books and what is current
In the books Struts 7.4.0

Converters extend ognl.DefaultTypeConverter or StrutsTypeConverter from org.apache.struts2.util, or implement OGNL’s TypeConverter

StrutsTypeConverter is in org.apache.struts2.util and extends the framework’s own DefaultTypeConverter, which implements org.apache.struts2.conversion.TypeConverter (the jar also contains OgnlTypeConverterWrapper and XWorkTypeConverterWrapper, which adapt between the two interfaces)

com.opensymphony.xwork2.conversion.TypeConversionException

org.apache.struts2.conversion.TypeConversionException

xwork-conversion.properties for application-wide converters

struts-conversion.properties; the old name is deprecated

Collection_xxx rules for the element class

Element_xxx; Collection_xxx is deprecated and a fallback only

Conversion annotations at method level only, with the rule prefix spelled out in class-level @Conversion

Field placement and key derivation (7.3.0)

Conversion errors reported as Invalid field value for field "xxx" with the key in xwork-messages.properties

The key xwork.default.invalid.fieldvalue is the same, in a bundle of the 7.4.0 jar; the per-field invalid.fieldvalue.<field> is unchanged

Dates "use the SHORT format of the locale"

A list of eight formats, including ISO forms (see Dates)

Request parameters fill any public setter of the action

Only setters annotated @StrutsParameter; the OGNL allowlist (struts.allowlist.enable, struts.allowlist.packageNames) also limits which classes OGNL can reach, so a model class outside the allowlist may not be readable from a page even when it was converted correctly

Struts 1 ConvertUtils and Converter with BeanUtils, a String property per field in an ActionForm, and string-to-number errors reported by hand in validate()

Struts 2 converts by itself into the typed property of the action or model; there is no ActionForm

Inconsistencies in the Official Documentation

Collected while running the examples against 7.4.0; none changes behaviour, but each can mislead:

  • The Type Conversion page says dates use "the SHORT format for the Locale"; DateConverter tries eight formats and java.time types use ISO formats (see Dates).

  • The page says a blank string "cannot be set on a BigDecimal or BigInteger"; in 7.4.0 a blank string gives null without an error (observed).

  • The Null Property Handling section refers to com.opensymphony.xwork2.interceptor.ParametersInterceptor; the class is org.apache.struts2.interceptor.parameter.ParametersInterceptor in 7.x. The same section also says "set it by calling setUsers() and then finally called getUsers().setName()" for the person example (the setter is setPerson).

  • The Advanced Example listing uses <stextfield name="beanList(%{bean.id}).name" /> (the tag prefix is missing a colon) and id="bean" on s:iterator (the retired attribute; var is current).

  • The TypeConversion Annotation page’s last example writes property = "java.util.Date"; the 7.4.0 annotation has no attribute property (its attributes are key, type, rule, converter, converterClass and value, per javap), and the page’s parameter table lists key and value but describes value only for KEY_PROPERTY.

  • The parameter table of the TypeConversion Annotation page marks converter as "DEPRECATED: either this or value" while the Javadoc of the annotation in the 7.4.0 jar carries defaults for every attribute (key and converter and value are the empty string, converterClass is XWorkBasicConverter); the example converterClass = String.class for an ELEMENT rule shows that the attribute doubles as the element class.

  • The Conversion Annotation page’s example (public setDate(Date date)) has a setter without a return type, which does not compile.

  • The page title of the guide page for annotations in older navigation, conversion-annotations, no longer exists (the address returns 404); the pages are conversion-annotation and type-conversion-annotation.

Summary

  • Conversion is a service of the OGNL layer: params sets properties by name, the converter turns text into the declared type, and failures are recorded in conversionErrors, not thrown.

  • The built-in converters are locale aware for numbers and dates; booleans are Boolean.valueOf; blank input is a non-error for most targets; the date format list is broader than the guide says.

  • Custom converters extend StrutsTypeConverter and throw TypeConversionException; they are registered per action (Action-conversion.properties), per bean (Class-conversion.properties) or per type for the whole application (struts-conversion.properties), or with @Conversion and @TypeConversion.

  • Collections and maps are filled from index notation; Element_, Key_, KeyProperty_ and CreateIfNull_ describe what the generics cannot; the () notation addresses an element by key; growth is limited by struts.ognl.autoGrowthCollectionLimit.

  • The conversionError interceptor turns the recorded failures into field errors with xwork.default.invalid.fieldvalue or invalid.fieldvalue.<field>, and keeps the text the visitor typed; workflow returns input.

  • The failure modes that lose data silently (comma as grouping, a date prefix, the growth limit, an excluded interceptor) are the ones to test for in a real form.

The related pages of this section are the value stack and OGNL (names and the expression syntax), parameter injection (what the request may set), validation (what to check after the types are right) and localization (the locale and the message bundles that this page uses), all linked in the introduction. Further related pages are Actions, Interceptors, Configuration, struts.xml and Constants and, for how JSP’s Expression Language coerces values (a different mechanism with a similar vocabulary), EL Operators and Type Coercion.

References