Affects: Spring 5.0.9 / Boot 2.0.5

We have a Spring Web Mvc REST service implemented using complex nested DTO models which are validated using @Valid on controller method signatures. We created a global RestControllerAdvice which uses @InitBinder to set it to direct field access. This seems to work as expected.

Now we have one controller, that has to deal with bean property style models and needs a different InitBinder setup. Having the hope of being able to override the global default one, by specifying it on the special controller itself, sadly failed with an error message of the InitBinder about an already initialized binding. Also there does not seem to exist some kind of exclude parameter on RestControllerAdvice that would allow us to exclude that special controller. Trying to use a special annotation on the other controllers and specifying it via annotations parameter on RestControllerAdvice also didn't work out during Mock Mvc Tests - ignoring the global one.

Finally, we ended up specifying @InitBinder on each controller separately - which works in all cases, but is quite cumbersome. Since we want to define the default globally and transparently for all controllers. Without having to remember, to specify packages, controller classes etc. on the global controller advice for new controllers to work properly - e.g. during validation.

Are there any plans to add some kind of exclude to the advice or being able to override a global default on controller level? This would be very helpful.

Comment From: rstoyanchev

Global @InitBinder methods from @ControllerAdvice are invoked first, and @InitBinder methods from the target controller are invoked second. That means it should be possible to override the Validator locally via WebDataBinder#setValidator. I confirmed this works as expected, so please explain how to reproduce or provide a sample.

Note that I tried with Boot 2.2. Version 2.0 is no longer supported. In any case I think the behavior with regards to the order has been that way from the beginning.

Comment From: vghero

Thanks for taking your time looking into this.

How does the validator relates to property-bean/direct-field access? If I take a look at this method, which we usually call to enable direct field access, I can't see any sign of validator setup. Is it somewhere hidden elsewhere?

    protected AbstractPropertyBindingResult createDirectFieldBindingResult() {
        DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(),
                getObjectName(), isAutoGrowNestedPaths());

        if (this.conversionService != null) {
            result.initConversion(this.conversionService);
        }
        if (this.messageCodesResolver != null) {
            result.setMessageCodesResolver(this.messageCodesResolver);
        }

        return result;
    }

It seems like DirectFieldBindingResult plays a role here? Isn't it like this is called after validation happens and constraint violations are processed?

Comment From: rstoyanchev

I'm not sure to be honest because I don't know the details of how your global and the local method initialize the DataBinder. Please provide those.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: vghero

So based on the "complete" example:

git clone https://github.com/spring-guides/gs-serving-web-content.git

I adapted/added:

@ControllerAdvice
public class GlobalInitBinderControllerAdvice {

    @InitBinder
    public void enableDefaultDirectFieldAccess(DataBinder dataBinder) {
        dataBinder.initDirectFieldAccess();
    }
}
@Controller
public class GreetingController {

    @PostMapping("/greeting")
    public void greeting(@RequestBody @Valid GreetingDto greeting) {
    }
}
@Controller
public class Greeting2Controller {

    @InitBinder
    public void overrideGlobalAdviceToBeanPropertyAccess(DataBinder dataBinder) {
        dataBinder.initBeanPropertyAccess();
    }

    @PostMapping("/greeting2")
    public void greeting(@RequestBody @Valid GreetingDto greeting) {
    }
}
public class GreetingDto {

    @NotBlank
    public String greeting;
}

If I now post an empty JSON to /greeting then I get a 400 back. If I post the same to /greeting2 I get a 500 back, complaining about the already set DataBinder:

DataBinder is already initialized - call initBeanPropertyAccess before other configuration methods",

So my question was: is there a way to get a global default InitBinder setup applied and just override edge cases in the appropriate controllers? Or is there a way to exclude those edgecases in the global controller advice?

Comment From: rstoyanchev

Thanks for clarifying. It's more clear now.

is there a way to get a global default InitBinder setup applied and just override edge cases in the appropriate controllers?

Yes, that is exactly how it works. A DataBinder instance is initialized with global @InitBinder methods first and then with @InitBinder methods from the target controller. The problem is that DataBinder does not allow alternating between direct field access and bean property access, so you can only set this once. This is because it delegates other calls to the BindingResult instance, e.g. for property editors and converters, but it doesn't have to work that way, so I'll turn this ticket into an enhancement request.

In the mean time, for a more convenient workaround than the one you already have, it could work to use a different name for the target object:

@ControllerAdvice
public class GlobalInitBinderControllerAdvice {

    @InitBinder("greetingDto")
    public void enableDefaultDirectFieldAccess(DataBinder dataBinder) {
        dataBinder.initDirectFieldAccess();
    }
}

@Controller
public class GreetingController {

    @PostMapping("/greeting")
    public void greeting(@RequestBody @Valid GreetingDto greeting) {
    }
}

@Controller
public class Greeting2Controller {

    @InitBinder("greeting2Dto")
    public void overrideGlobalAdviceToBeanPropertyAccess(DataBinder dataBinder) {
        dataBinder.initBeanPropertyAccess();
    }

    @PostMapping("/greeting2")
    public void greeting(@RequestBody @Valid Greeting2Dto greeting) {
    }
}

public class GreetingDto {

    @NotBlank
    public String greeting;
}

public class Greeting2Dto extends GreetingDto {
}

Comment From: vghero

Thanks for the feedback! That looks interesting. Will it also work with one @InitBinder without name and one with name? So we could apply the "special handling" to the named one and all other "default" cases can use the standard one without a name?

Comment From: rstoyanchev

The one without a name applies to all binders, so I'm afraid it can't work like that.