Search code examples
javaspring-securityspring-webflux

Bad return type in lambda expression: Mono<User> cannot be converted to Mono<UserDetails>


Why does this work

    @Bean
    public ReactiveUserDetailsService userDetailsService() {
        return username -> {
            Mono<UserDetails> userDetailsMono = Mono.justOrEmpty(userRepository.findByUsername(username));
            return userDetailsMono.switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("No such user: " + username)));
        };
    }

but this doesn't (which I would prefer as a more concise option)?

    @Bean
    public ReactiveUserDetailsService userDetailsService() {
        return username -> Mono.justOrEmpty(userRepository.findByUsername(username))
                .switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("No such user: " + username)));
    }
Bad return type in lambda expression: Mono<User> cannot be converted to Mono<UserDetails>

User is my custom user class that implements UserDetails. findByUsername() returns Mono<User>

// @Entity etc.
public class User implements UserDetails {
public interface UserRepository extends Repository<User, UUID> {
//...
    Optional<User> findByUsername(String username);

Solution

    • ReactiveUserDetailsService's abstract method, findByUsername(String username), returns Mono<UserDetails>. Go figure why it doesn't return Mono<? extends UserDetails>. It means your lambda should return Mono<UserDetails> too
    • Mono.justOrEmpty() is generic and has this contract: <T> Mono<T> justOrEmpty(@Nullable Optional<? extends T> data). It relies on Java's type inference which has its limitations
    • When you write this code:
        @Bean
        public ReactiveUserDetailsService userDetailsService() {
            return username -> Mono.justOrEmpty(userRepository.findByUsername(username));
        }
    

    the actual return type of justOrEmpty() is inferred from the return type of the wrapping function, fetchByUsername(). Which is Mono<UserDetails>

    • When you write this code:
        @Bean
        public ReactiveUserDetailsService userDetailsService() {
            return username -> {
                Mono<UserDetails> userDetailsMono = Mono.justOrEmpty(userRepository.findByUsername(username));
                return userDetailsMono.switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("No such user: " + username)));
            };
        }
    

    the actual return type of justOrEmpty() is inferred from the type of the variable you assign the result to. Which is, again, Mono<UserDetails>. Inferring from an expected return type and a variable type are two the most common ways Java's type inference works for generic methods

    • When you write this code
        @Bean
        public ReactiveUserDetailsService userDetailsService() {
            return username -> Mono.justOrEmpty(userRepository.findByUsername(username))
                    .switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("No such user: " + username)));
        }
    

    Java can infer justOrEmpty()'s return type neither from the return type of the outer function (you don't return the result of justOrEmpty()) nor a variable type (you don't assign the result of justOrEmpty()). So Java assumes the actual return type to be equal to the return type of the invoked function, findByUsername(). Which is Optional<User> (not Optional<UserDetails>)

    • switchIfEmpty() doesn't change the type of Mono, it stays Mono<User>. So in the end the lambda returns Mono<User> which is different from Mono<UserDetails> that the lambda should return
    • Mono<User> is not a subtype of Mono<UserDetails> even though User is a subtype of UserDetails (Java's generics are invariant)
    • You can also tell Java explicitly what type justOrEmpty() should return, sort of "turning off" the type inference. This works too:
        @Bean
        public ReactiveUserDetailsService userDetailsService() {
            return username -> Mono.<UserDetails>justOrEmpty(userRepository.findByUsername(username))
                    .switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("No such user: " + username)));
        }