Affects: 5.3.10


I am having a problem testing an argument resolution service I wrote on my own. Long story short, there seems not to be a simple, one-liner way to parse a complex nested object from query string in MVC (I'll recall the SO answer link if needed, I need to Google again for it).

So, long story short, I wrote my own ArgumentResolver and now I am unit-testing it for correct parsing of dates 😱😱. The dreaded ISO date/times.

I took inspiration from RequestParamMapMethodArgumentResolverTests for the tests, and from RequestParamMethodArgumentResolver for the implementation of my component, of which I won't paste the entire code in the main post for sake fo brevity.

 @BeforeEach
    public void setUp() throws Exception {
        LocaleContextHolder.resetLocaleContext();
        mavContainer = mock(ModelAndViewContainer.class);
        parameter = new MethodParameter(getClass().getMethod("handleExampleTestFilter", ExampleTestFilter.class), 0);
        httpRequest = new MockHttpServletRequest();
        webRequest = new ServletWebRequest(httpRequest);

        ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
        bindingInitializer.setConversionService(new DefaultFormattingConversionService());
        bindingInitializer.setAutoGrowNestedPaths(true);
        binderFactory = new DefaultDataBinderFactory(bindingInitializer);

        uut = new FilterArgumentResolver();
        // Expose request to the current thread (for SpEL expressions)
        RequestContextHolder.setRequestAttributes(webRequest);
    }

    /**
     * Sample no-op function
     */
    public void handleExampleTestFilter(ExampleTestFilter filter) {
        //NOOP
    }


  @Test
    void resolveArgument() {
        OffsetDateTime value = OffsetDateTime.of(1986, 06, 02, 15, 55, 24, 0, ZoneOffset.UTC);
        httpRequest.setParameter("filter.exampleOffsetDateTime.eq", "1986-06-02T15:55:24Z");
        Object resolved = assertDoesNotThrow(() -> uut.resolveArgument(parameter, mavContainer, webRequest, binderFactory));

        assertAll(
                () -> assertThat(resolved, is(notNullValue())),
                () -> assertThat(resolved, is(instanceOf(ExampleTestFilter.class))),
                () -> assertThat(resolved, hasProperty("exampleOffsetDateTime", is(notNullValue()))),
                () -> assertThat(resolved, hasProperty("exampleOffsetDateTime", hasProperty("eq", is(notNullValue())))),
                () -> assertThat(resolved, hasProperty("exampleOffsetDateTime", hasProperty("eq", comparesEqualTo(value))))
        );

    }


    @Data
    public static class ExampleTestFilter implements Filter {

        private OffsetDateTimeFilter exampleOffsetDateTime;

        private LocalDateFilter exampleLocalDate;

    }

Exception is

org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.OffsetDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.OffsetDateTime] for value '1986-06-02T15:55:24Z'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
    at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:79)
    at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:45)
    at org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:688)
    at it.orbit.common.web.filtering.FilterArgumentResolver.convertArg(FilterArgumentResolver.java:152)
    at it.orbit.common.web.filtering.FilterArgumentResolver.resolveArgument(FilterArgumentResolver.java:125)
    at it.orbit.common.web.filtering.FilterArgumentResolverTest.lambda$resolveArgument$1(FilterArgumentResolverTest.java:94)
    at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:72)
    at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:59)
    at org.junit.jupiter.api.Assertions.assertDoesNotThrow(Assertions.java:3120)
    at it.orbit.common.web.filtering.FilterArgumentResolverTest.resolveArgument(FilterArgumentResolverTest.java:94)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy5.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.OffsetDateTime] for value '1986-06-02T15:55:24Z'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:129)
    at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)
    ... 92 common frames omitted
Caused by: java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:223)
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
    ... 95 common frames omitted
Caused by: java.time.format.DateTimeParseException: Text '1986-06-02T15:55:24Z' could not be parsed at index 2
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
    at java.base/java.time.OffsetDateTime.parse(OffsetDateTime.java:402)
    at org.springframework.format.datetime.standard.TemporalAccessorParser.doParse(TemporalAccessorParser.java:126)
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:85)
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:50)
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:217)
    ... 96 common frames omitted

Isolating

Let's take a reproducible example. It takes the previously-created binderFactory (which is an argument of every HandlerMethodArgumentResolver)


    protected <T> T convertArg(WebDataBinderFactory binderFactory, NativeWebRequest webRequest, String propertyName, Class<T> propertyType, String propertyStringValue) throws ConversionNotSupportedException, TypeMismatchException {
        WebDataBinder binder;
        try {
            binder = binderFactory.createBinder(webRequest, null, propertyName);
        } catch (Exception e) {
            throw new RuntimeException("Error creating WebDataBinder: " + e.getMessage(), e);
        }
        return binder.convertIfNecessary(propertyStringValue, propertyType);
    }



    @Test
    void testParseDateTime() {
        String propertyStringValue = "1986-06-02T15:55:24Z";
        OffsetDateTime dateTime = OffsetDateTime.parse(propertyStringValue);
        OffsetDateTime dateTime1 = uut.convertArg(binderFactory, webRequest, "offsetDateTime", OffsetDateTime.class, propertyStringValue);
        assertThat(dateTime1, comparesEqualTo(dateTime));
    }


This method should convert an ISO string in an OffsetDateTime


Failed to convert value of type 'java.lang.String' to required type 'java.time.OffsetDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.OffsetDateTime] for value '1986-06-02T15:55:24Z'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.OffsetDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.OffsetDateTime] for value '1986-06-02T15:55:24Z'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
    at app//org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:79)
    at app//org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:45)
    at app//org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:688)
    at app//it.orbit.common.web.filtering.FilterArgumentResolver.convertArg(FilterArgumentResolver.java:152)
    at app//it.orbit.common.web.filtering.FilterArgumentResolverTest.testParseDateTime(FilterArgumentResolverTest.java:110)
    at java.base@11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base@11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base@11.0.12/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base@11.0.12/java.lang.reflect.Method.invoke(Method.java:566)
    at app//org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at app//org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at app//org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at app//org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    at app//org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at app//org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at app//org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base@11.0.12/java.util.ArrayList.forEach(ArrayList.java:1541)
    at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base@11.0.12/java.util.ArrayList.forEach(ArrayList.java:1541)
    at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
    at java.base@11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base@11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base@11.0.12/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base@11.0.12/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy5.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
    at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.OffsetDateTime] for value '1986-06-02T15:55:24Z'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
    at app//org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
    at app//org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
    at app//org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:129)
    at app//org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)
    ... 87 more
Caused by: java.lang.IllegalArgumentException: Parse attempt failed for value [1986-06-02T15:55:24Z]
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:223)
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
    ... 90 more
Caused by: java.time.format.DateTimeParseException: Text '1986-06-02T15:55:24Z' could not be parsed at index 2
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
    at java.base/java.time.OffsetDateTime.parse(OffsetDateTime.java:402)
    at org.springframework.format.datetime.standard.TemporalAccessorParser.doParse(TemporalAccessorParser.java:126)
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:85)
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:50)
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:217)
    ... 91 more

Debug

Digging into debug, we end at a point in org.springframework.format.datetime.standard.TemporalAccessorParser where there is a formatter, which happens to be

result = {DateTimeFormatter@4506} "Localized(SHORT,SHORT)"
 printerParser = {DateTimeFormatterBuilder$CompositePrinterParser@4518} "(Localized(SHORT,SHORT))"
 locale = {Locale@4387} "it_IT"
 decimalStyle = {DecimalStyle@4388} "DecimalStyle[0+-.]"
 resolverStyle = {ResolverStyle@4519} "SMART"
 resolverFields = null
 chrono = {IsoChronology@4390} "ISO"
 zone = null

Note that the locale is it_IT by default. The date should be formatted as 02/06/1986 according to locale rules.

Problem

At least without instantiating the full stack of Spring Boot, copying the test from the RequestParamMapResolver stuff, there seems to be no way to set the locale/format for the WebDataBinderFactory, which likely takes, somewhere, the default system language.

This has the derimental effect that when you need to be language-neutral (e.g. in parsing a GET request), you end up using the system language in parsing dates.

By exploring the code used to initialize the ConfigurableWebBindingInitializer there doesn't seem to be a way to exclude language from consideration when converting formattable values.

Possible solutions

TBD??

Well, at least by default add a language neutral formatter like ISO 8601 along with more language-specific formatters?

Comment From: djechelon

Here is the conversion service. I'll hide this post to reduce noise

package it.orbit.common.web.filtering;

import it.orbit.common.data.filtering.Filter;
import it.orbit.common.data.filtering.SimpleFilter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

import static java.text.MessageFormat.format;
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.*;

/**
 * Argument resolver that parses the query string for nested filters
 * <p>
 * USE:
 * Create a filter class that implements {@link Filter}
 * Only use {@link SimpleFilter} or its extensions as properties
 * Add to any MVC method as argument
 * Prefix all properties with "filter." in the query string
 * <p>
 * <p>
 * The resolver will always attempt to instantiate not-null values. If no filter parameter is specified, it should
 * resolve to an empty (not null) filter, whose nested properties are all null.
 * No guarantee that {@link SimpleFilter} properties are not null if none of their attributes are specified
 * <p>
 * The resolver is safe, in the sense that failure to match a property doesn't necessarily result in an exception, but
 * the property is ignored and logged.
 * <p>
 * Example
 * <pre>
 *     public class UserFilter implements Filter {
 *
 *         protected IntegerFilter id;
 *         protected StringFilter firstName;
 *         protected StringFilter lastName;
 *         protected StringFilter emailAddress;
 *
 *         protected OffsetDateTimeFilter created;
 *         protected IntegerFilter creator;
 *         protected OffsetDateTimeFilter modified;
 *         protected IntegerFilter modifier;
 *
 *     }
 *
 * </pre>
 * <p>
 * Avoid using any flat property type. Extend SimpleFilter as required
 */
@Slf4j
public class FilterArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return Filter.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        Class<? extends Filter> returnType = (Class<? extends Filter>) parameter.getParameterType();

        MultiValuedMap<String, String> argumentMap = new ArrayListValuedHashMap<>();
        for (Iterator<String> it = webRequest.getParameterNames(); it.hasNext(); ) {
            String paramName = it.next();
            if (!startsWith(paramName, "filter."))
                continue;
            else {
                String propertyName = substringAfter(paramName, "filter.");
                List<String> propertyValues = asList(webRequest.getParameterValues(paramName));
                argumentMap.putAll(propertyName, propertyValues);
                if (log.isTraceEnabled())
                    log.trace("Found compatible filter property [{}: {}]", propertyName, join(propertyValues, ","));
            }
        }
        Filter ret;
        try {
            ret = returnType.getConstructor().newInstance();
        } catch (InvocationTargetException e) {
            Throwable targetException = e.getTargetException();
            log.error(targetException.getMessage(), targetException);
            throw new RuntimeException("Error invoking constructor of filter class " + returnType.getSimpleName() + ": " + targetException.getMessage(), targetException);
        } catch (NoSuchMethodException e) {
            log.error(e.getMessage(), e);
            throw new UnsupportedOperationException(format("Filter class {0} requires a parameterless constructor, but none is found", returnType.getSimpleName()));
        } catch (ReflectiveOperationException ex) {
            log.error(ex.getMessage(), ex);
            throw new RuntimeException("Unable to instantiate filter object " + returnType.getSimpleName(), ex);
        }
        for (Map.Entry<String, Collection<String>> entry : argumentMap.asMap().entrySet()) {
            String nestedPropertyPath = entry.getKey();
            List<String> stringValues = new ArrayList(entry.getValue());
            String finalPropertyName = substringAfterLast(nestedPropertyPath, ".");
            if (log.isTraceEnabled())
                log.trace("Attempting to assign nested property {}", nestedPropertyPath);

            try {
                preAssignNestedProperties(ret, nestedPropertyPath);

                Class<?> nestedPropertyType = PropertyUtils.getPropertyType(ret, nestedPropertyPath);
                if (nestedPropertyType.isArray()) {
                    //Logic for arrays
                    Object args = Array.newInstance(nestedPropertyType.getComponentType(), stringValues.size());
                    for (int i = 0; i < stringValues.size(); i++) {
                        String stringValue = stringValues.get(i);
                        Object arg = convertArg(binderFactory, webRequest, finalPropertyName, nestedPropertyType.getComponentType(), stringValue);
                        Array.set(args, i, arg);
                    }
                    PropertyUtils.setNestedProperty(ret, nestedPropertyPath, args);

                } else {
                    //Logic for flat values
                    String stringValue = stringValues.get(0);
                    Object arg = convertArg(binderFactory, webRequest, finalPropertyName, nestedPropertyType, stringValue);
                    PropertyUtils.setNestedProperty(ret, nestedPropertyPath, arg);
                }
            } catch (ConversionNotSupportedException ex) {
//                throw new MethodArgumentConversionNotSupportedException(null, ex.getRequiredType(),
//                        finalPropertyName, parameter, ex.getCause());
                log.warn("Conversion not supported for {}", nestedPropertyPath, ex);
            } catch (TypeMismatchException ex) {
//                throw new MethodArgumentTypeMismatchException(null, ex.getRequiredType(),
//                        finalPropertyName, parameter, ex.getCause());
                log.warn("Type mismatch for {}", nestedPropertyPath, ex);
            } catch (ReflectiveOperationException ex) {
                log.error("Reflection error for {}", nestedPropertyPath, ex);
            } catch (RuntimeException ex) {
                log.error("At property path {}", nestedPropertyPath, ex);
            }
        }
        return ret;
    }

    protected <T> T convertArg(WebDataBinderFactory binderFactory, NativeWebRequest webRequest, String propertyName, Class<T> propertyType, String propertyStringValue) throws ConversionNotSupportedException, TypeMismatchException {
        WebDataBinder binder;
        try {
            binder = binderFactory.createBinder(webRequest, null, propertyName);
        } catch (Exception e) {
            throw new RuntimeException("Error creating WebDataBinder: " + e.getMessage(), e);
        }
        return binder.convertIfNecessary(propertyStringValue, propertyType);
    }

    protected void preAssignNestedProperties(Object target, String nestedPath) throws ReflectiveOperationException {
        if (target == null) throw new IllegalArgumentException("target must not be null");
        if (isBlank(nestedPath)) throw new IllegalArgumentException("nestedPath must not be blank");
        String[] paths = split(nestedPath, ".");
        if (paths.length < 2) {
            if (log.isTraceEnabled())
                log.trace("Property path {} is flat");
            return;
        }
        Object bean = target;
        for (int i = 0; i < paths.length - 1; i++) {
            if (isBlank(paths[i]))
                throw new IllegalArgumentException("Bad path fragment in " + nestedPath + " at index " + i);

            Class<?> propertyType = PropertyUtils.getPropertyType(bean, paths[i]);
            try {
                Object pathValue = propertyType.getConstructor().newInstance();
                PropertyUtils.setProperty(bean, paths[i], pathValue);
                bean = pathValue;
            } catch (ReflectiveOperationException ex) {
                throw new RuntimeException(format("Reflective error setting property {0} to bean of type {1}", paths[i], bean.getClass()), ex);
            }
        }
    }

}

Comment From: djechelon

Update: I have started the fully fledged application with the following configuration

spring:
  mvc:
    format:
      date-time: iso

Request GET http://localhost:8080/api/v1/admin/users?pageIndex=0&pageSize=25&filter.created.eq=1986-06-02T00:00:00Z results in different error

Caused by: java.time.format.DateTimeParseException: Text '1986-06-02T00:00:00Z' could not be parsed, unparsed text found at index 19
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2049) ~[na:na]
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948) ~[na:na]
    at java.base/java.time.OffsetDateTime.parse(OffsetDateTime.java:402) ~[na:na]
    at org.springframework.format.datetime.standard.TemporalAccessorParser.doParse(TemporalAccessorParser.java:126) ~[spring-context-5.3.10.jar:5.3.10]
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:85) ~[spring-context-5.3.10.jar:5.3.10]
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:50) ~[spring-context-5.3.10.jar:5.3.10]
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:217) ~[spring-context-5.3.10.jar:5.3.10]

Request GET http://localhost:8080/api/v1/admin/users?pageIndex=0&pageSize=25&filter.created.eq=1986-06-02T00:00:00 (with UTC's Z) results in different error

Caused by: java.time.format.DateTimeParseException: Text '1986-06-02T00:00:00' could not be parsed: Unable to obtain OffsetDateTime from TemporalAccessor: {},ISO resolved to 1986-06-02T00:00 of type java.time.format.Parsed
    at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:2017) ~[na:na]
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1952) ~[na:na]
    at java.base/java.time.OffsetDateTime.parse(OffsetDateTime.java:402) ~[na:na]
    at org.springframework.format.datetime.standard.TemporalAccessorParser.doParse(TemporalAccessorParser.java:126) ~[spring-context-5.3.10.jar:5.3.10]
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:85) ~[spring-context-5.3.10.jar:5.3.10]
    at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:50) ~[spring-context-5.3.10.jar:5.3.10]
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:217) ~[spring-context-5.3.10.jar:5.3.10]
    ... 97 common frames omitted
Caused by: java.time.DateTimeException: Unable to obtain OffsetDateTime from TemporalAccessor: {},ISO resolved to 1986-06-02T00:00 of type java.time.format.Parsed
    at java.base/java.time.OffsetDateTime.from(OffsetDateTime.java:370) ~[na:na]
    at java.base/java.time.format.Parsed.query(Parsed.java:235) ~[na:na]
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948) ~[na:na]
    ... 102 common frames omitted
Caused by: java.time.DateTimeException: Unable to obtain ZoneOffset from TemporalAccessor: {},ISO resolved to 1986-06-02T00:00 of type java.time.format.Parsed
    at java.base/java.time.ZoneOffset.from(ZoneOffset.java:348) ~[na:na]
    at java.base/java.time.OffsetDateTime.from(OffsetDateTime.java:359) ~[na:na]

Comment From: GoossensMichael

I have the same issue but with properties in a yaml file. The parsing always happens with the Localized(SHORT,) DateTimeFormatter and there does not seem a way to configure this.

Comment From: nithril

Same, would be more generic and flexible to use ISO format to parse properties than using a formatter with a Locale

I ended up making a custom converter:

public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
    @Override
    public LocalDateTime convert(String source) {
        return LocalDateTime.parse(source);
    }
}

As it is a Converter, Spring still tries to use the binding converter using the Locale, failed, then fallback to that one.

Comment From: snicoll

Shouldn't you be adding @DateTimeFormat to the target attribute to customize the format to use to parse the String value? If that's still applicable, can we please move all that text into a sample application that we can run ourselves? Thank you.

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: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.