§Handling form submission
Before you start with Play forms, read the documentation on the Play enhancer. The Play enhancer generates accessors for fields in Java classes for you, so that you don’t have to generate them yourself. You may decide to use this as a convenience. All the examples below show manually writing accessors for your classes.
§Enabling/Disabling the forms module
By default, Play includes the Java forms module (play-java-forms
) when enabling the PlayJava
SBT plugin, so there is nothing to enable if you already have enablePlugins(PlayJava)
on your project.
The forms module is also available in PlayImport
as javaForms
, which can be used with libraryDependencies += javaForms
in your build.sbt
.
Note: If you are not using forms, you can remove the forms dependency by using the
PlayMinimalJava
SBT plugin instead ofPlayJava
. This also allows you to remove several transitive dependencies only used by the forms module, including several Spring modules and the Hibernate validator.
§Defining a form
The play.data
package contains several helpers to handle HTTP form data submission and validation. The easiest way to handle a form submission is to define a play.data.Form
that wraps an existing class:
import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;
public class User {
protected String email;
protected String password;
protected FilePart<TemporaryFile> profilePicture;
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public FilePart<TemporaryFile> getProfilePicture() {
return profilePicture;
}
public void setProfilePicture(FilePart<TemporaryFile> pic) {
this.profilePicture = pic;
}
}
The above form defines an email
and a password
text field and a profilePicture
file input field, meaning the corresponding HTML form has to be defined with the multipart/form-data
encoding to be able to upload the file.
As you can see, by default, you have to define getter and setter methods so Play is able to access the Form fields. You can however also enable “direct field access” (for all forms) by setting play.forms.binding.directFieldAccess = true
in conf/application.conf
. In this mode Play will ignore the getter and setter methods and will try to directly access the fields:
import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;
public class User {
public String email;
public String password;
public FilePart<TemporaryFile> profilePicture;
}
Note: When using “direct field access” and a field is not accessible to Play during form binding (e.g. if a field or the class containing the field is not defined as
public
) Play will try to make a field accessible via reflection by callingfield.setAccessible(true)
internally. Depending on the Java version (8+), JVM and the Security Manager settings that could cause warnings about illegal reflective access or, in the worst case, throw aSecurityException
To wrap a class you have to inject a play.data.FormFactory
into your Controller which then allows you to create the form:
Form<User> userForm = formFactory.form(User.class);
Instead of enabling “direct field access” for all forms, you can enable it only for specific ones:
Form<User> userForm = formFactory.form(User.class).withDirectFieldAccess(true);
Note: The underlying binding is done using Spring data binder.
This form can generate a User
result value from a HashMap<String,String>
for the text data and from a Map<String, FilePart<?>>
for the file data:
Map<String,String> textData = new HashMap<>();
textData.put("email", "[email protected]");
textData.put("password", "secret");
Map<String, FilePart<?>> files = new HashMap<>();
files.put("profilePicture", myProfilePicture);
User user = userForm.bind(lang, attrs, textData, files).get();
If you have a request available in the scope, you can bind directly from the request content:
User user = userForm.bindFromRequest(request).get();
§Defining constraints
You can define additional constraints that will be checked during the binding phase using JSR-380
(Bean Validation 2.0) annotations:
public class User {
@Required
protected String email;
protected String password;
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
Tip: The
play.data.validation.Constraints
class contains several built-in validation annotations. All of these constraint annotations are defined as@Repeatable
. This lets you, for example, reuse the same annotation on the same element several times but each time with differentgroups
. For some constraints however it makes sense to let them repeat itself anyway, like@ValidateWith
/@ValidatePayloadWith
for example.
In the Advanced validation section further below you will learn how to handle concerns like cross field validation, partial form validation or how to make use of injected components (e.g. to access a database) during validation.
§Handling binding failure
Of course if you can define constraints, then you need to be able to handle the binding errors.
if (userForm.hasErrors()) {
return badRequest(views.html.form.render(userForm));
} else {
User user = userForm.get();
return ok("Got user " + user);
}
Typically, as shown above, the form simply gets passed to a template. Global errors can be rendered in the following way:
@if(form.hasGlobalErrors) {
<p class="error">
@for(error <- form.globalErrors) {
<p>@error.format(messages)</p>
}
</p>
}
Errors for a particular field can be rendered in the following manner with error.format
:
@for(error <- form("email").errors) {
<p>@error.format(messages)</p>
}
Note that error.format
takes messages()
as an argument – this is an play.18n.Messages
instance defined in JavaI18N.
§Filling a form with initial default values
Sometimes you’ll want to fill a form with existing values, typically for editing:
userForm = userForm.fill(new User("[email protected]", "secret"));
Tip:
Form
objects are immutable - calls to methods likebind()
andfill()
will return a new object filled with the new data.
§Handling a form with dynamic fields
You can use a DynamicForm
if you need to retrieve data from an html form with dynamic fields:
public Result hello(Http.Request request) {
DynamicForm requestData = formFactory.form().bindFromRequest(request);
String firstname = requestData.get("firstname");
String lastname = requestData.get("lastname");
return ok("Hello " + firstname + " " + lastname);
}
§Register a custom DataBinder
In case you want to define a mapping from a custom object to a form field string and vice versa you need to register a new Formatter for this object.
You can achieve this by registering a provider for Formatters
which will do the proper initialization.
For an object like JavaTime’s LocalTime
it could look like this:
import java.text.ParseException;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import java.time.LocalTime;
import play.data.format.Formatters;
import play.data.format.Formatters.SimpleFormatter;
import play.i18n.MessagesApi;
@Singleton
public class FormattersProvider implements Provider<Formatters> {
private final MessagesApi messagesApi;
@Inject
public FormattersProvider(MessagesApi messagesApi) {
this.messagesApi = messagesApi;
}
@Override
public Formatters get() {
Formatters formatters = new Formatters(messagesApi);
formatters.register(LocalTime.class, new SimpleFormatter<LocalTime>() {
private Pattern timePattern = Pattern.compile(
"([012]?\\d)(?:[\\s:\\._\\-]+([0-5]\\d))?"
);
@Override
public LocalTime parse(String input, Locale l) throws ParseException {
Matcher m = timePattern.matcher(input);
if (!m.find()) throw new ParseException("No valid Input", 0);
int hour = Integer.valueOf(m.group(1));
int min = m.group(2) == null ? 0 : Integer.valueOf(m.group(2));
return LocalTime.of(hour, min);
}
@Override
public String print(LocalTime localTime, Locale l) {
return localTime.format(DateTimeFormatter.ofPattern("HH:mm"));
}
});
return formatters;
}
}
After defining the provider you have to bind it:
import com.google.inject.AbstractModule;
import play.data.format.Formatters;
public class FormattersModule extends AbstractModule {
@Override
protected void configure() {
bind(Formatters.class).toProvider(FormattersProvider.class);
}
}
Finally you have to disable Play’s default FormattersModule
and instead enable your module in application.conf
:
play.modules.enabled += "com.example.FormattersModule"
play.modules.disabled += "play.data.format.FormattersModule"
When the binding fails an array of errors keys is created, the first one defined in the messages file will be used. This array will generally contain:
["error.invalid.<fieldName>", "error.invalid.<type>", "error.invalid"]
The errors keys are created by Spring DefaultMessageCodesResolver, the root “typeMismatch” is replaced by “error.invalid”.
§Advanced validation
Play’s built-in validation module is using Hibernate Validator under the hood. This means we can take advantage of features defined in the JSR-380
(Bean Validation 2.0). The Hibernate Validator documentation can be found here.
§Cross field validation
To validate the state of an entire object we can make use of class-level constraints.
To free you from the burden of implementing your own class-level constraint(s), Play out-of-the-box already provides a generic implementation of such constraint which should cover at least the most common use cases.
Now let’s see how this works: To define an ad-hoc validation, all you need to do is annotate your form class with Play’s provided class-level constraint (@Validate
) and implement the corresponding interface (in this case Validatable<String>
) - which forces you to override a validate
method:
import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
@Validate
public class User implements Validatable<String> {
@Constraints.Required
protected String email;
protected String password;
@Override
public String validate() {
if (authenticate(email, password) == null) {
// You could also return a key defined in conf/messages
return "Invalid email or password";
}
return null;
}
// getters and setters
}
The message returned in the above example will become a global error. Errors are defined as play.data.validation.ValidationError
.
Also be aware that in this example the validate
method and the @Constraints.Required
constraint will be called simultaneously - so the validate
method will be called no matter if @Constraints.Required
was successful or not (and vice versa). You will learn how to introduce an order later on.
As you can see the Validatable<T>
interface takes a type parameter which determines the return type of the validate()
method.
So depending if you want to be able to add a single global error, one error (which could be global as well) or multiple (maybe global) errors to a form via validate()
, you have to use either a String
, a ValidationError
or a List<ValidationError>
as type argument. Any other return types of the validate method will be ignored by Play.
If validation passes inside a validate()
method you must return null
or an empty List
. Returning any other non-null
value (including empty string) is treated as failed validation.
Returning a ValidationError
object may be useful when you have additional validations for a specific field:
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
@Validate
public static class LoginForm implements Validatable<ValidationError> {
// fields, getters, setters, etc.
@Override
public ValidationError validate() {
if (authenticate(email, password) == null) {
// Error will be displayed for the email field:
return new ValidationError("email", "Invalid credentials");
}
return null;
}
}
You can add multiple validation errors by returning List<ValidationError>
. This can be used to add validation errors for a specific field, global errors or even a mix of these options:
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import java.util.List;
@Validate
public static class SignUpForm implements Validatable<List<ValidationError>> {
// fields, getters, setters, etc.
@Override
public List<ValidationError> validate() {
final List<ValidationError> errors = new ArrayList<>();
if (authenticate(email, password) == null) {
// Add an error which will be displayed for the email field:
errors.add(new ValidationError("email", "Access denied"));
// Also add a global error:
errors.add(new ValidationError("", "Form could not be submitted"));
}
return errors;
}
}
As you can see, when using an empty string as the key of a ValidationError
it becomes a global error.
One more thing: Instead of writing out error messages you can use message keys defined in conf/messages
and pass arguments to them. When displaying the validation errors in a template the message keys and it’s arguments will be automatically resolved by Play:
// Global error without internationalization:
new ValidationError("", "Errors occurred. Please check your input!");
// Global error; "validationFailed" should be defined in `conf/messages` - taking two arguments:
new ValidationError("", "validationFailed", Arrays.asList(arg1, arg2));
// Error for the email field; "emailUsedAlready" should be defined in `conf/messages` - taking the email as argument:
new ValidationError("email", "emailUsedAlready", Arrays.asList(email));
§Partial form validation via groups
When a user submits a form there can be use cases where you don’t want to validate all constraints at once but just some of them. For example think about a UI wizard where in each step only a specified subset of constraints should get validated.
Or think about the sign-up and the login process of a web application. Usually for both processes you want the user to enter an email address and a password. So these processes would require almost the same forms, except for the sign-up process the user also has to enter a password confirmation. To make things more interesting let’s assume a user can also change his user data on a settings page when he is logged in already - which would need a third form.
Using three different forms for such a case isn’t really a good idea because you would use the same constraint annotations for most of the form fields anyway. What if you have defined a max-length constraint of 255 for a name
field and then want to change it to a limit of just 100? You would have to change this for each form. As you can imagine this would be error prone in case you forget to update one of the forms.
Luckily we can simply group constraints:
import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import javax.validation.groups.Default;
@Validate(groups = {SignUpCheck.class})
public class PartialUserForm implements Validatable<ValidationError> {
@Constraints.Required(groups = {Default.class, SignUpCheck.class, LoginCheck.class})
@Constraints.Email(groups = {Default.class, SignUpCheck.class})
private String email;
@Constraints.Required
private String firstName;
@Constraints.Required
private String lastName;
@Constraints.Required(groups = {SignUpCheck.class, LoginCheck.class})
private String password;
@Constraints.Required(groups = {SignUpCheck.class})
private String repeatPassword;
@Override
public ValidationError validate() {
if (!checkPasswords(password, repeatPassword)) {
return new ValidationError("repeatPassword", "Passwords do not match");
}
return null;
}
// getters and setters
}
The SignUpCheck
and LoginCheck
group are defined as two interfaces:
public interface SignUpCheck { }
public interface LoginCheck { }
For the sign-up process we simply pass the SignUpCheck
group to the form(...)
method:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, SignUpCheck.class).bindFromRequest(request);
In this case the email address is required and has to be a valid email address, both the password and the password confirmation are required and the two passwords have to be equal (because of the @Validate
annotation which calls the validate
method). But we don’t care about the first name and last name - they can be empty or we could even exclude these input fields in the sign up page.
For the login process we just pass the LoginCheck
group instead:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, LoginCheck.class).bindFromRequest(request);
Now we only require the email address and the password to be entered - nothing more. We don’t even care about if the email is valid. You probably wouldn’t display any of the other form fields to the user because we don’t validate them anyway.
Imagine we also have a page where the user can change the user data (but not the password):
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, Default.class).bindFromRequest(request);
Which is exactly the same as:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class).bindFromRequest(request);
In this case following constraints will be validated: The email address is required and has to be valid plus the first name and last name are required as well - that is because if a constraint annotation doesn’t explicitly define a group
then the Default
group is used.
Be aware we don’t check any of the password constraints: Because they explicitly define a group
attribute but don’t include the Default
group they won’t be taken into account here.
As you can see in the last example, when only passing the group javax.validation.groups.Default
you can omit it - because it’s the default anyway.
But as soon you pass any other group(s) you would also have to pass the Default
group explicitly if you want any of it’s fields taken into account during the validation process.
Tip: You can pass as many groups as you like to the
form(...)
method (not just one). Just to be clear: These groups will then be validated all at once - not one after the other.
For advanced usage a group of constraints can include another group. You can do that using group inheritance.
§Defining the order of constraint groups
You can validate groups in sequences. This means groups will be validated one after another - but the next group will only be validated if the previous group was validated successfully before. (However right now it’s not possible to determine the order of how constraints will be validated within a group itself - this will be part of a future version of Bean Validation)
Based on the example above let’s define a group sequence:
import javax.validation.GroupSequence;
import javax.validation.groups.Default;
@GroupSequence({ Default.class, SignUpCheck.class, LoginCheck.class })
public interface OrderedChecks { }
Now we can use it:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, OrderedChecks.class).bindFromRequest(request);
Using this group sequence will first validate all fields belonging to the Default
group (which again also includes fields that haven’t defined a group at all). Only when all the fields belonging to the Default
group pass validation successfully, the fields belonging to the SignUpCheck
will be validated and so on.
Using a group sequence is especially a good practice when you have a validate
method which queries a database or performs any other blocking action: It’s not really useful to execute the method at all if the validation fails at it’s basic level (email is not valid, number is a string, etc). In such a case you probably want the validate
be called only after checking all other annotation-based constraints before and only if they pass. A user, for example, who signs up should enter a valid email address and only if it is valid a database lookup for the email address should be done afterwards.
§Passing payloads to validators
If needed, you can also pass a ValidationPayload
object - which contains useful information sometimes needed for a validation process - to a validate
method.
To pass such a payload just annotate your form with @ValidateWithPayload
(instead of just @Validate
) and implement ValidatableWithPayload
(instead of just Validatable
):
import java.util.Map;
import com.typesafe.config.Config;
import play.data.validation.Constraints.ValidatableWithPayload;
import play.data.validation.Constraints.ValidateWithPayload;
import play.data.validation.ValidationError;
import play.data.validation.ValidationPayload;
import play.i18n.Lang;
import play.i18n.Messages;
@ValidateWithPayload
public class ChangePasswordForm implements ValidatableWithPayload<ValidationError> {
// fields, getters, setters, etc.
@Override
public ValidationError validate(ValidationPayload payload) {
Lang lang = payload.getLang();
Messages messages = payload.getMessages();
Map<String, Object> ctxArgs = payload.getArgs();
TypedMap attrs = payload.getAttrs();
Config config = payload.getConfig();
// ...
}
}
§Custom class-level constraints with DI support
Sometimes you need more sophisticated validation processes. E.g. when a user signs up you want to check if his email address already exists in the database and if so validation should fail.
Because constraints support both runtime Dependency Injection and , we can easily create our own custom (class-level) constraint which gets a Database
object injected - which we can use later in the validation process. Of course you can also inject other components like MessagesApi
, JPAApi
, etc.
Note: You only need to create one class-level constraint for each cross concern. For example, the constraint we will create in this section is reusable and can be used for all validation processes where you need to access the database. The reason why Play doesn’t provide any generic class-level constraints with dependency injected components is because Play doesn’t know which components you might have enabled in your project.
First let’s set up the interface with the validate
method we will implement in our form later. You can see the method gets passed a Database
object (Checkout the database docs):
- Without Payload
-
import play.db.Database; public interface ValidatableWithDB<T> { public T validate(final Database db); }
- With Payload
-
import play.db.Database; import play.data.validation.Constraints.ValidationPayload; public interface ValidatableWithDB<T> { public T validate(final Database db, final ValidationPayload payload); }
We also need the class-level annotation we put on our form class:
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Repeatable(ValidateWithDB.List.class)
@Constraint(validatedBy = ValidateWithDBValidator.class)
public @interface ValidateWithDB {
String message() default "error.invalid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* Defines several {@code @ValidateWithDB} annotations on the same element.
*/
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface List {
ValidateWithDB[] value();
}
}
Finally this is how our constraint implementation looks like:
- Without Payload
-
import javax.inject.Inject; import javax.validation.ConstraintValidatorContext; import play.data.validation.Constraints.PlayConstraintValidator; import play.db.Database; public class ValidateWithDBValidator implements PlayConstraintValidator<ValidateWithDB, ValidatableWithDB<?>> { private final Database db; @Inject public ValidateWithDBValidator(final Database db) { this.db = db; } @Override public void initialize(final ValidateWithDB constraintAnnotation) { } @Override public boolean isValid(final ValidatableWithDB<?> value, final ConstraintValidatorContext constraintValidatorContext) { return reportValidationStatus(value.validate(this.db), constraintValidatorContext); } }
- With Payload
-
import javax.inject.Inject; import javax.validation.ConstraintValidatorContext; import play.data.validation.Constraints.PlayConstraintValidator; import play.data.validation.Constraints.PlayConstraintValidatorWithPayload; import play.data.validation.Constraints.ValidationPayload; import play.db.Database; public class ValidateWithDBValidator implements PlayConstraintValidatorWithPayload<ValidateWithDB, ValidatableWithDB<?>> { private final Database db; @Inject public ValidateWithDBValidator(final Database db) { this.db = db; } @Override public void initialize(final ValidateWithDB constraintAnnotation) { } @Override public boolean isValid(final ValidatableWithDB<?> value, final ValidationPayload payload, final ConstraintValidatorContext constraintValidatorContext) { return reportValidationStatus(value.validate(this.db, payload), constraintValidatorContext); } }
Note: Don’t get confused with
ValidationPayload
andConstraintValidatorContext
: The former class is provided by Play and is what you use in your day-to-day work when dealing with forms in Play. The latter class is defined by the Bean Validation specification and is used only internally in Play - with one exception: This class emerges when your write your own custom class-level constraints, where you only need to pass it on to thereportValidationStatus
method however anyway.
As you can see we inject the Database
object into the constraint’s constructor and use it later when calling validate
. When using runtime Dependency Injection, Guice will automatically inject the Database
object, but for compile-time Dependency Injection you need to do that by yourself:
import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.data.FormFactoryComponents;
import play.data.validation.MappedConstraintValidatorFactory;
import play.db.DBComponents;
import play.db.HikariCPComponents;
import play.filters.components.NoHttpFiltersComponents;
import play.routing.Router;
public class ValidateWithDBComponents extends BuiltInComponentsFromContext
implements FormFactoryComponents, DBComponents, HikariCPComponents, NoHttpFiltersComponents {
public ValidateWithDBComponents(ApplicationLoader.Context context) {
super(context);
}
@Override
public Router router() {
return Router.empty();
}
@Override
public MappedConstraintValidatorFactory constraintValidatorFactory() {
return new MappedConstraintValidatorFactory()
.addConstraintValidator(
ValidateWithDBValidator.class,
new ValidateWithDBValidator(database("default"))
);
}
}
Note: you don’t need to create the
database
instance by yourself, it is already defined in the implemented interfaces.
This way, your validator will be available when necessary.
When writing your own class-level constraints you can pass following objects to the reportValidationStatus
method: A ValidationError
, a List<ValidationError>
or a String
(handled as global error). Any other objects will be ignored by Play.
Finally we can use our custom class-level constraint to validate a form:
- Without Payload
-
import play.data.validation.Constraints; import play.data.validation.ValidationError; import play.db.Database; @ValidateWithDB public class DBAccessForm implements ValidatableWithDB<ValidationError> { @Constraints.Required @Constraints.Email private String email; @Constraints.Required private String firstName; @Constraints.Required private String lastName; @Constraints.Required private String password; @Constraints.Required private String repeatPassword; @Override public ValidationError validate(final Database db) { // Access the database to check if the email already exists if (User.byEmail(email, db) != null) { return new ValidationError("email", "This e-mail is already registered."); } return null; } // getters and setters }
- With Payload
-
import play.data.validation.Constraints; import play.data.validation.ValidationError; import play.data.validation.Constraints.ValidationPayload; import play.db.Database; @ValidateWithDB public class DBAccessForm implements ValidatableWithDB<ValidationError> { @Constraints.Required @Constraints.Email private String email; @Constraints.Required private String firstName; @Constraints.Required private String lastName; @Constraints.Required private String password; @Constraints.Required private String repeatPassword; @Override public ValidationError validate(final Database db, final ValidationPayload payload) { // Access the database to check if the email already exists if (User.byEmail(email, db) != null) { return new ValidationError("email", "This e-mail is already registered."); } return null; } // getters and setters }
Tip: You might have recognized that you could even implement multiple interfaces and therefore add multiple class-level constraint annotations on your form class. Via validation groups you could then just call the desired validate method(s) (or even multiple at once during one validation process).