Hello,
I’ve been testing things with Forms on Play 2.8.2 and I think the validation of sub-forms is a bit problematic. I wanted to know if I was doing it wrong or if it was done like that on purpose.
Here is what I’ve done :
@Constraints.Validate
public class SubForm implements Constraints.Validatable<List<ValidationError>> {
@Constraints.Required
protected String name;
@Constraints.Required
protected Integer value;
// Getters,Setters
@Override
public List<ValidationError> validate() {
List<ValidationError> errors = new ArrayList();
// Error for test purpose.
errors.add(new ValidationError("value", "Invalid value"));
return errors.isEmpty() ? null : errors;
}
}
import javax.validation.Valid;
@Constraints.Validate
public class MainForm implements Constraints.Validatable<List<ValidationError>> {
@Constraints.Required
protected String foo;
@Constraints.Required
@Valid
protected List<SubForm> entries;
// Getters,Setters
@Override
public List<ValidationError> validate() {
List<ValidationError> errors = new ArrayList();
// Validation but let's say it has no error.
return errors.isEmpty() ? null : errors;
}
}
The controller
public class MyController extends AController {
private final FormFactory formFactory;
@Inject
public MyController(final MessagesApi messagesApi, final FormFactory formFactory) {
this.formFactory = formFactory;
}
public Result POST_SubmitForm(final Http.Request request) {
final Form<MainForm> boundForm = this.formFactory.form(MainForm.class).bindFromRequest(request);
if (boundForm.hasErrors()) {
return Results.badRequest(boundForm.errorsAsJson());
}
// Logic if the form is ok.
return Results.noContent();
}
}
So … With that code, what happens is that when the route is called, the MainForm.validate
method is called (which is normal). BUT with the @Valid
, the SubForm.validate
methods is also called for EACH entry. So if I send 5 entries, that’s great ! I can validate them independently.
BUT !
If I add an error with new ValidationError("value", "Invalid value.");
, I get the following error on the console.
play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[IllegalStateException: JSR-303 validated property 'entries[2]' does not have a corresponding accessor for data binding - check your DataBinder's configuration (bean property versus direct field access)]]
at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:359)
at play.api.http.DefaultHttpErrorHandler.onServerError(HttpErrorHandler.scala:261)
at play.core.server.AkkaHttpServer$$anonfun$2.applyOrElse(AkkaHttpServer.scala:429)
at play.core.server.AkkaHttpServer$$anonfun$2.applyOrElse(AkkaHttpServer.scala:421)
at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:453)
at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:56)
at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:93)
at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94)
at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:93)
at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:48)
at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:48)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
Caused by: java.lang.IllegalStateException: JSR-303 validated property 'entries[2]' does not have a corresponding accessor for data binding - check your DataBinder's configuration (bean property versus direct field access)
at play.data.Form.addConstraintViolationToBindingResult(Form.java:929)
at play.data.Form.lambda$bind$10(Form.java:1028)
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
at play.data.Form.bind(Form.java:1028)
at play.data.Form.bindFromRequest(Form.java:665)
at controllers.api.MyController.POST_SubmitForm(MyController.java:46)
at router.Routes$$anonfun$routes$1.$anonfun$applyOrElse$14(Routes.scala:199)
at play.core.routing.HandlerInvokerFactory$$anon$8.resultCall(HandlerInvoker.scala:150)
at play.core.routing.HandlerInvokerFactory$$anon$8.resultCall(HandlerInvoker.scala:149)
at play.core.routing.HandlerInvokerFactory$JavaActionInvokerFactory$$anon$3$$anon$4$$anon$5.invocation(HandlerInvoker.scala:115)
at play.core.j.JavaAction$$anon$1.call(JavaAction.scala:119)
at play.http.DefaultActionCreator$1.call(DefaultActionCreator.java:33)
at play.core.j.JavaAction.$anonfun$apply$8(JavaAction.scala:175)
at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:671)
at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:430)
at play.core.j.HttpExecutionContext.$anonfun$execute$1(HttpExecutionContext.scala:64)
at play.api.libs.streams.Execution$trampoline$.execute(Execution.scala:70)
at play.core.j.HttpExecutionContext.execute(HttpExecutionContext.scala:59)
at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:392)
at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:302)
at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:276)
at scala.concurrent.impl.Promise$DefaultPromise.map(Promise.scala:146)
at scala.concurrent.Future$.apply(Future.scala:671)
at play.core.j.JavaAction.apply(JavaAction.scala:176)
at play.api.mvc.Action.$anonfun$apply$4(Action.scala:82)
at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:433)
... 12 common frames omitted
Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'value' of bean class [forms.api.MainForm]: Bean property 'value' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:622)
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:612)
at org.springframework.validation.AbstractPropertyBindingResult.getActualFieldValue(AbstractPropertyBindingResult.java:104)
at org.springframework.validation.AbstractBindingResult.rejectValue(AbstractBindingResult.java:117)
at play.data.Form.lambda$addConstraintViolationToBindingResult$7(Form.java:912)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at play.data.Form.addConstraintViolationToBindingResult(Form.java:910)
... 37 common frames omitted
To me it looks like Play want to make it work … but for some reason… it doesn’t.
Btw, if instead, I create the error this way:
@Override
public List<ValidationError> validate() {
List<ValidationError> errors = new ArrayList();
// Error for test purpose.
errors.add(new ValidationError("entries[0].value", "Invalid value"));
return errors.isEmpty() ? null : errors;
}
It work ! The problem is that I put 0
without knowing the actual index of the entry.
If someone have some information on that, it would be really appreciated !
Thank you everyone !