Form with a list of sub-form

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 !

I’ve done some more tests, and it look like that the standard validation like @Constraints.Required do actually work as intended.

But when trying to validate it inside the validate method with a list of ValidationError, it does not (or at least I’m not aware of a way to make it work).

How does your setter for

    protected List<SubForm> entries;

look like?

Hello Matthias.

That would be like

  public List<SubForm> getEntries() {
    return this.entries;
  }

  public void setEntries(List<SubForm> entries) {
    this.entries = entries;
  }