spring-projects / spring-projects/spring-framework

Different behavior of Kotlin constructor binding of validated form bean model attributes

Open
#36,535 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
60.2k
Forks
38.8k
Avg merge
5d 2h
Merged PRs (30d)
27

Description

While using constructor binding of validated form beans (aka command objects or model attributes) as Kotlin classes we noticed that they are handled differently, compared to property binding.

When invalid data is submitted to the controller, binding results and model contents behave differently depending on the type of binding used.

Upon the initialization of form beans, invalid data may cause a TypeMismatchException to be thrown during conversion (invalid data being data that cannot be converted, like trying to read "aaa" as a number).

Example form bean:

class FormBeanConstructor(val input: Double? = null)

Should that happen, for Kotlin constructor binding, it looks like the binding result object is created earlier, and the target object of the validation will be set to null.

The problem is that if the failed arguments are optional (should they have default values), the constructor can still be called.

The binding result then contributes the null target object to the model attributes, which causes the observed behavior that the controller argument exists, but is null on the model.

The result of this behavior is that the controller receives a constructed form bean as an argument, but if we access the model to retrieve the form bean (which we expect should be the same object), it will be set to null. Also the target of the binding result is also null.

class FormBeanConstructor(val input: Double? = null)

@PostMapping("/constructor")
@ResponseBody
fun constructor(@Valid @ModelAttribute("formBean") formBean: FormBeanConstructor, result: BindingResult, model: Model): Double? {
	if (result.hasErrors()) {
		check(formBean == model.getAttribute("formBean")) // fails, model.getAttribute("formBean") is null
		check(formBean == result.target) // fails, result.target is null
	}
	return formBean.input
}

By contrast, when using property binding, the controller works as we would reasonable expect:

class FormBeanProperty {
	val input: Double? = null
}

@PostMapping("/property")
@ResponseBody
fun property(@Valid @ModelAttribute("formBean") formBean: FormBeanJava2, result: BindingResult, model: Model): Double? {
	if (result.hasErrors()) {
		check(formBean == model.getAttribute("formBean")) // succeeds
		check(formBean == result.target) // succeeds
	}
	return formBean.input
}

We suspect the problem may be due to the eager creation of the binding results object, which should probably be delayed to the point we can expect the target object to exist.

We have provided a sample project at https://github.com/jpmsilva/constructor-binding-validation showing the different cases, should it help better understand the issue.

At this time the workaround we found is to revert back to property binding for form beans that need validation.

As a side note, we are trying to follow the recommendation to use constructor binding:

Another good practice is to apply constructor binding, which uses only the request parameters it needs for constructor arguments, and any other input is ignored. This is in contrast to property binding which by default binds every request parameter for which there is a matching property.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked sample project, then inspect the referenced binding flow in spring-context/src/main/java/org/springframework/validation/DataBinder.java and spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java. Compare constructor and property binding with invalid numeric input, and confirm that the constructed form bean is consistently used as the model attribute and BindingResult target.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin, spring
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.