When user can't interact with the application, he loses the interest for it. It's why forms and other input methods are a useful to satisfy the final user demand. Play Framework also supports operations on forms, as generation from POJO or dynamic validation with annotations.
Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I wrote
one on that topic! You can read it online
on the O'Reilly platform,
or get a print copy on Amazon.
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
In the first part of the article, we'll see how to implement forms based on POJO models in Play Framework. The second part will be destined to customized parts, as validator or supplementary fields. At the end we'll see how the form behaves against values which aren't associated with the model.
Forms and validation in Play Framework
To see how does the form work, we need to create a new table in the database. As in the case of sessions in Play Framework, we'll write a evolution script and call it 2.sql:
# --- !Ups CREATE TABLE IF NOT EXISTS users ( id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, login VARCHAR(10) NOT NULL, passa VARCHAR(255) NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id) ) ENGINE=InnoDB; # --- !Downs DROP TABLE users;
As you can see, it creates users table with some basic fields. Now, we must create JPA entity:
@Entity @Table(name="users") public class User { private int id; @Required(message = "Login is mandatory field") @MinLength(value = 5, message = "Login must be at least 5-characters text") @MaxLength(value = 10, message = "Login can be 10-characters max length") @Pattern(value = "[a-z0-9\\-]+", message = "Only alphanumerical lower case characters and - are allowed in this field.") private String login; @Required(message = "Password is mandatory field") private String password; private Date birthday; private Date createdTime; @Id @GeneratedValue(strategy = IDENTITY) @Column(name="id") public int getId() { return this.id; } @Column(name="login") public String getLogin() { return this.login; } @Column(name="passa") public String getPassword() { return this.password; } @Temporal(TIMESTAMP) @Column(name="created") public Date getCreatedTime() { if (this.createdTime == null) { setCreatedTime(new Date()); } return this.createdTime; } @Temporal(DATE) @Column(name="birthday") public Date getBirthday() { return this.birthday; } @Transient public String getSalt() { return String.valueOf(this.login.charAt(4))+this.birthday.getTime()+String.valueOf(this.login.charAt(0)); } public void setId(int id) { this.id = id; } public void setLogin(String login) { this.login = login; } public void setPassword(String password) { this.password = password; } public void setCreatedTime(Date createdTime) { this.createdTime = createdTime; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public String toString() { return "User {id: "+this.id+", login: "+this.login+"}"; } }
Class structure is simple. It's composed only by setters and getters. Play-based things are only the annotations on private fields. Yes, there are some validation annotations:
- @Required to annotate required field
- @Maxlength and @Minlength to handle, respectively, max and min length of the field value
- @Pattern to decide which characters are allowed to the field
The forms in Play are created with play.data.Form<T> object. Take a look on our UserController to see it:
public class UserController extends Controller { /** * This Form object is common for all controllers. It's a empty version of register form. We create the version of form * handled by submit method (registerSubmit) with userForm.bindFromRequest(). */ private static Form<User> userForm = Form.form(User.class); @Transactional(readOnly=true) public static Result register() { return ok(register.render(userForm)); } @Transactional() public static Result registerSubmit() { Form<User> submittedForm = userForm.bindFromRequest(); if (!submittedForm.hasErrors()) { User user = submittedForm.get(); UserService userService = (UserService) ServicesInstances.USER_SERVICE.getService(); boolean added = userService.addNewUser(user); if (added) { // TODO : redirect to success page } else { // TODO : pass error message to the template } Logger.debug("Found user :"+user); } return ok(register.render(submittedForm)); } }
As you can see, the controller stores a private static userForm object. This object is shared by all users calling register page (view rendered by register() method). This register form is the same for all users. And it's a safe because it changes only after the user submits it. This moment is handled in registerSubmit() method where we create a new form depending on the executed request (userForm.bindFromRequest()). Play contains several different bind() methods. Some of them supports JSON or request values, the others Map<String, String[]> instances. They all point internally to public Form<T> bind(Map<String,String> data, String... allowedFields) method. As the name of this method suggest, it's responsible for handling submitted data. It means that inside this method we can find a fragments about:
received data normalization (translation the unordered data to interpreted object, in this case DataBinder instance):
DataBinder dataBinder = null; Map<String, String> objectData = data; if(rootName == null) { dataBinder = new DataBinder(blankInstance()); } else { dataBinder = new DataBinder(blankInstance(), rootName); objectData = new HashMap<String,String>(); for(String key: data.keySet()) { if(key.startsWith(rootName + ".")) { objectData.put(key.substring(rootName.length() + 1), data.get(key)); } } }
validation
SpringValidatorAdapter validator = new SpringValidatorAdapter(play.data.validation.Validation.getValidator()); dataBinder.setValidator(validator); dataBinder.setConversionService(play.data.format.Formatters.conversion); dataBinder.setAutoGrowNestedPaths(true); dataBinder.bind(new MutablePropertyValues(objectData)); Set
> validationErrors; if (groups != null) { validationErrors = validator.validate(dataBinder.getTarget(), groups); } else { validationErrors = validator.validate(dataBinder.getTarget()); } decision if data is correct or not
BindingResult result = dataBinder.getBindingResult(); for (ConstraintViolation<Object> violation : validationErrors) { String field = violation.getPropertyPath().toString(); FieldError fieldError = result.getFieldError(field); if (fieldError == null || !fieldError.isBindingFailure()) { try { result.rejectValue(field, violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), getArgumentsForConstraint(result.getObjectName(), field, violation.getConstraintDescriptor()), violation.getMessage()); } catch (NotReadablePropertyException ex) { throw new IllegalStateException("JSR-303 validated property '" + field + "' does not have a corresponding accessor for data binding - " + "check your DataBinder's configuration (bean property versus direct field access)", ex); } } }
and finally, the initialization of new Form instance containing treated data
return new Form(rootName, backedType, data, errors, None(), groups);
If you're familiar with Spring Framework, the code snippet saw in the previous list should hit your eyes. Yes, it's the same as in the case of validation in Spring. Even more, Play uses Spring objects to work with errors (BindingResult) or handle request data (DataBinder).
Customize forms in Play Framework
Play can handle two types of error:
normal errors
"Normal errors" are the result of standard validation process. This standard validation process is translated by the errors on annotated fields. For example, "normal error" is an error which occurs when we try to submit empty field annotated with @Required constraint. You can see it in the second to last point from the previous list.
global errors
Global errors are the result of invocation of validate() method in the POJO model class. This method was removed in the first part of this article. If we want to handle errors in more global manner, we can add it as the very first method of the User class:
/** * This is a ad-hoc validation method, invoked only when the "basic" validation (ie. annotation-based) hasn't errors. * * @return List of ValidationError. Returns null when they're no errors. */ public List<ValidationError> validate() { Logger.debug("Validating user on validate() method"); UserService userService = (UserService) ServicesInstances.USER_SERVICE.getService(); boolean isUniqueLogin = userService.isUniqueLogin(this.login); if (!isUniqueLogin) { List<alidationError> errors = new ArrayList<ValidationError>(); errors.add(new ValidationError("login", "This login already exists")); return errors; } return null; }
As you can see in our case, we invoke this method to check if the login submitted by the user is unique. validate() method is called every time when generic on-fields validators didn't dedect any error:
BindingResult result = dataBinder.getBindingResult(); // ... field errors detection here, see the second to last point of previous list if(result.hasErrors()) { // ... translates Spring error objects (FieldError) to Play error objects (ValidationError) } else { Object globalError = null; if(result.getTarget() != null) { try { java.lang.reflect.Method v = result.getTarget().getClass().getMethod("validate"); globalError = v.invoke(result.getTarget()); } catch(NoSuchMethodException e) { } catch(Throwable e) { throw new RuntimeException(e); } } if(globalError != null) { // ... because validate method can return as well String as List<ValidationError> or Map<String,List<ValidationError>>, // some cast (the case of the returned Map - globalError is an Object and Map<String,List<ValidationError>> is expected) or // translation (String or List<ValidationError> to Map<String,List<ValidationError>>) are made here return new Form(rootName, backedType, data, errors, None(), groups); }
This validate() method was the first way of customize Play Framework. Another way consist on create our own validator. As you can see in the entity class, they're a birthday field. We want that it remains valid. To achieve it, we can create a validator and accept only dates in the past. To do that, we must start by writing annotation:
@Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = BeforeDateValidator.class) @play.data.Form.Display(name="BeforeDate") public @interface BeforeDate { String message() default "The date can't be a future date"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String dateToCompare() default BeforeDateValidator.NOW; }
Only dateToCompare attribute is particular. As its name indicates, it stores date used to comparison with the date introduced by the user. In this case, this date corresponds to today. And it's normal in the case of birthday date because we wont the date being before today (so in the past). The BeforeDateValidator is based on this dependency to decide if one date is valid or not:
public class BeforeDateValidator extends Constraints.Validator<Date> implements ConstraintValidator<BeforeDate, Date> { public static final String NOW = "now"; private Date dateToCompare; @Override public void initialize(BeforeDate annot) { if (annot.dateToCompare().equals(NOW)) { dateToCompare = new Date(); } } @Override public Tuple<String, Object[]> getErrorMessageKey() { return new Tuple("beforeDate", new Object() {}); } @Override public boolean isValid(Date toValid) { Logger.debug("Validating "+toValid); return toValid != null && toValid.before(dateToCompare); } }
Now, we should annotate a field validated with @BeforeDate constraint. However, it won't be enough. Play doesn't know the way to handle Date fields. To handle them properly and, allow the validation, we need to create an formatter. After this formatted will be applied to fields annotated with @Birthday annotation:
@Target({FIELD,METHOD}) @Retention(RUNTIME) public @interface Birthday { String format() default "dd/MM/yyyy"; }
The formatter is an instance of AnnotationFormatter (it depends on one annotation) and it bases the conversion (Date <=> String) on format attribute of @Birthday annotation:
public class BirthdayFormatter extends AnnotationFormatter<Birthday, Date> { /** * Converts form input to expected Java's object. * * @return Converted Date or null if an exception occurred. */ @Override public Date parse(Birthday annot, String input, Locale locale) throws ParseException { try { return FromStringConverter.toDate(input, annot.format()); } catch (Exception e) { return null; } } /** * Converts Java object to form input. * * @return String representation of Date object or empty String if an exception occurred. */ @Override public String print(Birthday annot, Date dateObj, Locale locale) { try { return FromDateConverter.toString(dateObj, annot.format()); } catch (Exception e) { return ConstantsContainer.EMPTY_STRING; } } }
The last thing to do is to activate the BirthdayFormatter. We should to register it in one method called every time when the application starts and restarts. We opt for a static bloc inside configuration class (StoreGlobalSettings):
static { Logger.debug("------------- Setting formatters ---------------"); Formatters.register(Date.class, new BirthdayFormatter()); };
Now, if you look carefully in the logs, you should see following entry at the application setup:
[debug] application - ------------- Setting formatters ---------------
And this one when you submit register form:
[debug] application - Validating Wed Nov 20 00:00:00 CET 2013 [debug] application - Validating user on validate() method
Mass assignment in Play Framework
Forms in Play Framework are nice and simple (as in Spring by the way), but there're some drawback too. The main is the risk of mass assignment. A mass assignment is an attack consisting on submitting fields belonging to form Java object which shouldn't be accepted as input values. To understand this better, imagine our User class with a supplementary field: isMajor. The value of this field is specified after register by administrator. If this value is true, user will receive some gifts every time when he shows his activity in the site. Let's make some tests by adding this field in the database and User class:
private int major; @Column(name="major") public int getMajor() { return this.major; } public void setMajor(int major) { this.major = major; } @Transient public boolean isMajor() { return this.major == 1; }
Our form remains at the same state. Let's see what happens if we create manually <input type="hidden" name="major" value="1" /> and submit it into registerSubmit() method (we added simple debug before UserService call Logger.debug("Data is :"+submittedForm.data()) and Logger.debug("User from form is: "+user)) :
[debug] application - Data is :{register=register, birthday=20/11/2013, login=bartosz4, password=bartosz4, major=1} [debug] application - User from form is: User {id: 0, login: bartosz2, is major: true}
As you can see, user is major before the administrator decision. One of protections against this kind of dangers is the precision of accepted form fields in bindFromRequest method, as following:
@Transactional() public static Result registerSubmit() { Form<User> submittedForm = userForm.bindFromRequest("login", "password", "birthday");
Thanks to it, only login, password and birthday fields will be interpreted by the form. So even if you try to bypass it by adding major field, it won't be taken in consideration by the bind() method saw in the first part of this article:
[debug] application - Data is :{register=register, birthday=20/11/2013, login=bartosz4, password=bartosz4, major=1} [debug] application - User from form is:User {id: 0, login: bartosz4, is major: false}
This article shows how form works in Play Framework. The first part presented the way of integrating JPA entities (or another Java objects) to Play's form mechanism. A mechanism which is composed by Spring validation components. After that we discovered how to customize forms in Play with annotations, formatters and global validation methods. The last part explained a little bit about mass assignment danger.
Consulting

With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects