Skip to content

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

@jpmsilva

Description

@jpmsilva

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions