Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NotReadablePropertyException due to mismatch between ConstraintViolation property path and BindingResult target in MethodValidationAdapter #31746

Closed
k-seth opened this issue Dec 4, 2023 · 7 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression
Milestone

Comments

@k-seth
Copy link
Contributor

k-seth commented Dec 4, 2023

The title is pretty vague, sorry - couldn't think of something better. There is plenty of detail here to make up for that though.

Spring 6.1.x brought method bean validation (#30645) - as part of the feedback to this enhancement, I provided a PR #31530 to attempt to refine this further.

In hindsight, the solution I provided also has its problems. Essentially, all the issues stem from the use of the leaf bean - whose behaviour is, unfortunately, not as I had understood at the time. As I have come to release, the change I submitted had some unintended side effects that only became clear as I fully started migrating to Spring Boot 3.2.

1. Cascaded validation can fail
In cases where a validation error occurs on a nested bean, SpringValidatorAdopt#getRejectedValue may attempt to get the raw value for the property. As a result, one will see an error like JSR-303 validated property 'professor.name' does not have a corresponding accessor for Spring data binding. This is because the code attempts to walk the full property path, starting from the provided bean. Unfortunately, the leaf bean points to the nested bean with the cascaded validation, not the initial bean where validation started. Thus, the property path is invalid from this context (Go figure, despite giving the exact bean in question, it is no good). The JSR error is not thrown in all cases, as paths which include collections with non-indexed/keyed access are fine because the getRejectedValue method explicitly skips walking paths with [] in them - instead, it uses the value on the violation directly - because there is no way to access the element in question.

As an example, consider the following code:

public void registerCourse(@Valid Course course) {
}

private record Course(@NotBlank String title, @Valid Set<Person> students, @Valid Person professor) {
}

private record Person(@Size(min = 1, max = 10) String name, List<@NotBlank String> hobbies) {
}

The failure can be caused by having a validation error on the professor parameter - say the name, as my error above uses.

After looking at the information available in MethodValidationAdaptor, I'm not sure how to get that root bean for all reasonable data types - specifically types like Set where there is no direct way to access the element. The leaf bean was supposed to solve that issue. It is unfortunate that the ConstraintViolation doesn't expose this either, given that it had to have known the bean to produce the path nodes in the first place. At this point, I'm not sure where else to try drawing this information from. I wonder if it is worth looking at the downstream constraint violating processing to work this behaviour in there instead if nothing better comes up.

The next two are more minor in my opinion. I found them accidentally while debugging rather than because they strictly posed a problem.

2. Container element result argument incorrect when validating non-field object error
Since the leaf bean appears to represent the last bean accessed, the opposite problem to 1 can happen. Rather than the leaf bean being too precise, it isn't precise enough, though here it doesn't cause an error. Essentially, in the case of a situation like

public void addHobbies(@Valid Set<String> hobbies) {
}

the leafBean will represent the service/controller/etc. that has the parameter since there is no other bean to set the leaf to. As a result, all violations will be under the same ParameterErrors, rather than having a ParameterErrors for each element with violations. Because of this, it should only impact types which rely on the leaf bean to provide the unique side of the BeanResultKey. Even then, it only means that the documented behaviour is broken. All the violations are there, so no information is lost - the structure is just a bit off.

All container types are impacted in some way, however, as the argument set on the parameter error will be incorrect, regardless of whether the errors are split by element or not.

I haven't had a chance to dig too deep into this one, as a solution for problem 1 may address this too. However, I did consider a few things:

  • Could be possible to check if the next node is of type CONTAINER_ELEMENT, and then simply pass the invalid value as the bean
    • Unsure if this would misbehave with cascaded validation, but it does seem like it might be fragile
    • Also, for basic types like String, is it fine to be lumping these under the bean validation umbrella? Even if not, can an invalid string and say a null passed for a custom Bean type be reasonably distinguished?

3. ParameterErrors may incorrectly set the container parameter
In the case of cascaded validation the condition here

this.container = (arg != null && !arg.equals(leafBean) ? arg : null);

can be evaluated as true, even if there is no container. Once again, the doing of my friend the leaf bean.

Using the same example code from problem 1, if one of the student elements has a validation error when calling registerCourse, the leaf bean will be for the Person while the arg is the Course. The Course will then be set as a container even though it isn't one. (Note this example only works because of the Set type, which sidesteps problem 1.)

In this case, there could be a solution that relies on neither the leaf bean nor the arg class. Path.Node has a few subtypes (BeanNode, ContainerElementNode and PropertyNode) which expose a method to check if a node is in a container. I had some success with something like:

private static boolean isContainerElement(Path.Node node) {
	return switch (node.getKind()) {
		case BEAN -> node.as(Path.BeanNode.class).getContainerClass() != null;
		case PROPERTY -> node.as(Path.PropertyNode.class).getContainerClass() != null;
		case CONTAINER_ELEMENT -> true;
		default -> false;
	};
}

I've added some additional tests on my forked repo to exercise this and ensure the container is properly set, so I could provide that pretty easily. That said, I am making some assumption that since the others don't provide this getContainerClass, it is reasonable to expect them not to be container elements.

If it is okay, I'd like your perspective on this @rstoyanchev, since we discussed the original change I provided. I'd for sure like to continue contributing - though I clearly need to spend more time to get more context around any changes to avoid a mishap like this again.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 4, 2023
@rstoyanchev
Copy link
Contributor

Thanks for the follow-up report @k-seth. The code snippets seem to match the tests in MethodValidationAdapterTests. Could you provide a snippet with the actual tests as well for the above cases?

@rstoyanchev rstoyanchev added the in: core Issues in core modules (aop, beans, core, context, expression) label Dec 4, 2023
@rstoyanchev rstoyanchev self-assigned this Dec 4, 2023
k-seth added a commit to k-seth/spring-framework that referenced this issue Dec 5, 2023
@k-seth
Copy link
Contributor Author

k-seth commented Dec 5, 2023

Here are a few test cases to demonstrate each of the problems:

k-seth@c0035aa#diff-84f5eb0790cab0b59468f24f88055fe660b4342c385a6774e033779dc9dadfd0R256

I provided a comment to demonstrate roughly where and why each test case fails. I also included (though did not enable) the node-based container detection I mentioned as a potential option for problem 3.

@k-seth
Copy link
Contributor Author

k-seth commented Dec 15, 2023

Seems like #31835 is a related issue - I'll try taking another look at this to see if I can offer some further input.

@rstoyanchev
Copy link
Contributor

Indeed, #31835 is a mismatch between a full ConstraintViolation path and a BindingResult with a target that is at a lower level within the target object structure. In that case a top-level object Dogs that contains List<Dog> list. The property path is list[0].name but the target in object is the Dog instance.

I'm having a closer look myself at what we changed for #31530, and what we could do to fix handling of violations on nested objects.

@rstoyanchev rstoyanchev added in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 19, 2023
@rstoyanchev rstoyanchev added this to the 6.1.3 milestone Dec 19, 2023
@rstoyanchev rstoyanchev changed the title Use of leaf bean in Method Bean Validation can cause bad behaviour Mismatch between ConstraintViolation property path and BindingResult target in MethodValidationAdapter Dec 19, 2023
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Dec 20, 2023

I've had a closer look.

Indeed, for the value unwrapping feature of bean validation supported through a ValueExtractor, the only container objects we can support are lists, arrays, and maps. We cannot access the unwrapped value for any other method parameter that is a container of some kind.

If bean validation exposed the unwrapped value for a Path.Node we would be able to support any container. As it is the best we can do is get the next node after the method parameter or return value node, see if it has a container index or container key, and extract it from the argument. The leafBean as we now know is at the bottom of the property path chain, so not at all relevant.

I will restore something close to the previous behavior we had, and add extra tests.

rstoyanchev added a commit that referenced this issue Dec 20, 2023
The goal for #31530 was to support bean validation on Set and other
method parameters that are containers of value(s) for which there is
a registered Jakarta Validation ValueExtractor.

Unfortunately, bean validation does not expose the unwrapped value
for a Path.Node, which we need for a method parameter in order to
create a BindingResult for the specific bean within the container,
and the leafBean that we tried to use is really the node at the
very bottom of the property path (i.e. not what we need).

This change removes the use of beanLeaf, restores the logic as it
was before, adds support for arrays, and a new test class for
scenarios with cascaded violations.

See gh-31746
@rstoyanchev rstoyanchev changed the title Mismatch between ConstraintViolation property path and BindingResult target in MethodValidationAdapter NotReadablePropertyException due to mismatch between ConstraintViolation property path and BindingResult target in MethodValidationAdapter Dec 20, 2023
@k-seth
Copy link
Contributor Author

k-seth commented Dec 20, 2023

Hm, thanks for looking into it Rossen. Sadly, I don't have much else to add. It is disappointing that there is no obvious way to offer parity to using @Validated under the current implementation, but it is what it is. That said, it might be possible to also support Optional - there is a ValueExtractor for it, and while it is not an iterable type, it can be unwrapped to get the nested element (if present).

Lastly, your commit 0a94dce referenced my attempt to expand collection support, and so it expanded the check to include Collection as the assignable type. Is the change going to cause issues now that generic collection support has been dropped? Should it instead align on the types that the adapter supports?

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Dec 21, 2023

Thanks for the review. I will make those adjustments.

For the longer term, I created jakartaee/validation#194 to see if an improvement would be considered.

rstoyanchev added a commit that referenced this issue Dec 21, 2023
After the updates to MethodValidationAdapter in commit d7ce13 related
to method validation on element containers, we also need to adjust
the checks in HandlerMethod when method validation applies.

See gh-31746
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

3 participants