Search code examples
javamysqlspringspring-bootthymeleaf

Thymeleaf: Field error when applying a role to a user (One to Many relationship)


I'm trying to make a Thymeleaf application that can register an account with an attached role that was chosen during registration. Here's a visualization. I will try to add as much relevant code as I can to give a better idea of what's happening and then explain the problem.

User.java:

package com.example.demo.model;

<imports>

@Entity
@Table(name = "user", uniqueConstraints = @UniqueConstraint(columnNames = "account"))
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    private String account;
    
    private String password;
    
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="role_id", nullable=false)
    private Role role;
    
    @OneToMany(mappedBy = "user")
    private Collection<Comment> comments;
    
    public User() {
        
    }

    public User(String firstName, String lastName, String account, String password, Role role,
            Collection<Comment> comments) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
        this.account = account;
        this.password = password;
        this.role = role;
        this.comments = comments;
    }

    <getters and setters>
    
}

Role.java:

package com.example.demo.model;

<imports>

@Entity
@Table(name = "role")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    @OneToMany(fetch=FetchType.LAZY, mappedBy = "role")
    private Set<User> users = new HashSet<User>(0);
    
    public Role() {
        
    }

    public Role(String name, Set<User> users) {
        super();
        this.name = name;
        this.users = users;
    }

    <getters and setters>
}

Comment.java:

<not important>

UserRepository.java:

package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.model.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    User findByAccount(String account);
    
}

RoleRepository.java:

package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.model.Role;

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    
}

UserService.java:

package com.example.demo.service;

<imports>

@Service("userService")
public class UserService {

    private UserRepository userRepository;
    
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    
    public UserService(UserRepository userRepository) {
        super();
        this.userRepository = userRepository;
    }
    
    public User findByAccount(String account) {
        return userRepository.findByAccount(account);
    }
    
    public void saveUser(User user) {
        userRepository.save(user);
    }
}

RoleService.java:

package com.example.demo.service;

<imports>

@Service
public class RoleService {
    
    private RoleRepository roleRepository;
    
    public RoleService(RoleRepository roleRepository) {
        super();
        this.roleRepository = roleRepository;
    }
}

UserRegistrationController.java:

package com.example.demo.web;

<imports>

@Controller
@RequestMapping("/registration")
public class UserRegistrationController {

    private UserService userService;
    
    @ModelAttribute("user")
    public User user() {
        return new User();
    }
    
    @Autowired
    RoleRepository roleRepository;
    
    @GetMapping
    public String showRegistrationForm(Model model) {
        model.addAttribute("roles", roleRepository.findAll());
        return "registration";
    }
    
    @PostMapping
    public String registerUserAccount(User user) {
        System.out.println();
        userService.saveUser(user);
        return "redirect:/registration?success";
    }
}

SecurityConfiguration.java (just in case):

package com.example.demo.config;

<imports>

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers(
            "/registration**",
            "/js/**",
            "/css/**",
            "/img/**").permitAll().anyRequest().authenticated().
            and().formLogin().loginPage("/login").permitAll().
            and().logout().invalidateHttpSession(true).clearAuthentication(true)
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/login?logout")
            .permitAll();
    }
    
}

Relevant code snippet from registration.html:

<form th:action="@{/registration}" method="post" th:object="${user}">
    <div class="form-group">
        <label class="control-label" for="firstName">First name</label> <input
            id="firstName" class="form-control" th:field="*{firstName}"
            required autofocus="autofocus" />
    </div>

    <div class="form-group">
        <label class="control-label" for="lastName">Last name</label> <input
            id="lastName" class="form-control" th:field="*{lastName}"
            required autofocus="autofocus" />
    </div>

    <div class="form-group">
        <label class="control-label" for="account">Account</label> <input
            id="account" class="form-control" th:field="*{account}" required
            autofocus="autofocus" />
    </div>

    <div class="form-group">
        <label class="control-label" for="password">Password</label> <input
            id="password" class="form-control" type="password"
            th:field="*{password}" required autofocus="autofocus" />
    </div>
    
    <div class="form-group">
        <label class="control-label" for="role">Role</label> 
        <select class="form-control" th:field="*{role}" id="role">
            <option th:each="role: ${roles}" th:value="${role}" th:text="${role.name}"></option>
        </select>
    </div>

    <div class="form-group">
        <button type="submit" class="btn btn-success">Register</button>
        <span>Already registered? <a href="/" th:href="@{/login}">Login
                here</a></span>
    </div>
</form>

The problem: Whenever I try to create a user with the selected role I get the following error:

Field error in object 'user' on field 'role': rejected value [com.example.demo.model.Role@6e32df30]; codes [typeMismatch.user.role,typeMismatch.role,typeMismatch.com.example.demo.model.Role,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.role,role]; arguments []; default message [role]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.example.demo.model.Role' for property 'role'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'com.example.demo.model.Role@6e32df30'; nested exception is java.lang.NumberFormatException: For input string: "com.example.demo.model.Role@6e32df30"]]

So, I imagine what my code should be doing is using a Role value we get from the form (${role}) and applying it to the Role variable in User.java, connecting them in the relationship. Instead what seemingly happens is that instead of taking a Role value it's taking a String value of "com.example.demo.model.Role@[random id]" from the form.

I'm still very new to Spring Boot and Thymeleaf. Does anyone know what's the problem? I've tried to look up solutions to this exact problem for many hours but I still can't find anything that helps me. Thanks in advance.


Solution

  • Okay, I looked around some more and finally found the resolution myself. Turns out I had to pass th:value="${role.id}" instead of "${role}". Here's the changes I did:

    UserRegistrationController.java:

    package com.example.demo.web;
    
    <imports>
    
    @Controller
    @RequestMapping("/registration")
    public class UserRegistrationController {
    
        private UserService userService;
    
        public UserRegistrationController(UserService userService) {
            super();
            this.userService = userService;
        }
        
        @Autowired
        RoleRepository roleRepository;
        
        @GetMapping
        public String showRegistrationForm(Model model, User user) {
            model.addAttribute("roles", roleRepository.findAll());
            return "registration";
        }
        
        @PostMapping
        public String registerUserAccount(@Valid @ModelAttribute("user") User user, BindingResult result) {
            userService.saveUser(user);
            return "redirect:/registration?success";
        }
    }
    

    Relevant code snippet from registration.html:

    <form th:action="@{/registration}" method="post" th:object="${user}">
        <div class="form-group">
            <label class="control-label" for="firstName">First name</label> <input
                id="firstName" class="form-control" th:field="*{firstName}"
                required autofocus="autofocus" />
        </div>
    
        <div class="form-group">
            <label class="control-label" for="lastName">Last name</label> <input
                id="lastName" class="form-control" th:field="*{lastName}"
                required autofocus="autofocus" />
        </div>
    
        <div class="form-group">
            <label class="control-label" for="account">Account</label> <input
                id="account" class="form-control" th:field="*{account}" required
                autofocus="autofocus" />
        </div>
    
        <div class="form-group">
            <label class="control-label" for="password">Password</label> <input
                id="password" class="form-control" type="password"
                th:field="*{password}" required autofocus="autofocus" />
        </div>
        
        <div class="form-group">
            <label class="control-label" for="role">Role</label> 
            <select class="form-control" th:field="*{role}" id="role">
                <option th:each="role: ${roles}" th:value="${role.id}" th:text="${role.name}"></option>
            </select>
        </div>
    
        <div class="form-group">
            <button type="submit" class="btn btn-success">Register</button>
            <span>Already registered? <a href="/" th:href="@{/login}">Login
                    here</a></span>
        </div>
    </form>
    

    Hope this helps anyone who was similarly confused.