Home > Enterprise >  Null fields in Thymeleaf form using Spring Boot, other fields fine
Null fields in Thymeleaf form using Spring Boot, other fields fine

Time:02-16

I'm relatively new to Spring Boot. Currently, I'm making a Spring Boot application with user registration system but I've run into an issue. Some of the fields in a form are registering as ‘null’ on the back end, despite the request being posted correctly.

I have a HTML/ Thymeleaf form which submits 8 fields to create a 'User' object. This is the form:

<form th:action="@{/users/register_attempt}" th:object="${user}"
      method="post" style="max-width: 600px; margin: 0 auto;">
    <div >
        <div >
            <label >DOB: </label>
            <div >
                <input type="text" th:field="*{dob}"  th:required="required" />
            </div>
        </div>
        <div >
            <label >First Name: </label>
            <div >
                <input type="text" th:field="*{name}"  th:required="required" />
            </div>
        </div>

        <div >
            <label >Surname: </label>
            <div >
                <input type="text" th:field="*{surname}"  th:required="required" />
            </div>
        </div>

        <div >
            <label >PPSN: </label>
            <div >
                <input type="text" th:field="*{ppsn}"  th:required="required" />
            </div>
        </div>
        <div >
            <label >Address: </label>
            <div >
                <input type="text" th:field="*{address}"  th:required="required" />
            </div>
        </div>
        <div >
            <label >Phone Number: </label>
            <div >
                <input type="number" th:field="*{phone}"   />
            </div>
        </div>

        <div >
            <label >E-mail: </label>
            <div >
                <input type="email" th:field="*{email}"  th:required="required" />
            </div>
        </div>
        <div >
            <label >Password: </label>
            <div >
                <input type="password" th:field="*{password}" 
                       required minlength="6" maxlength="10" th:required="required"/>
            </div>
        </div>

        <div>
            <button type="submit" >Sign Up</button>
        </div>
    </div>
</form>

And here is the model that the form code is meant to mimic:

public class User {
@Id
@GeneratedValue
private Long id;

@NotBlank
private String dob;
@NotBlank
private String name;
@NotBlank
private String surname;
@NotBlank
private String ppsn;
@NotBlank
private String address;
@NotBlank
private String phone;
@NotBlank
@Column(unique = true)
private String email;

private String nextApptId;

private String dose1Date;

private String dose2Date;

private String lastLogin;
@NotBlank
private String password;

public User() {
    super();
}

public User(String dob, String name, String surname, String ppsn, String address, String phone, String email, String password) {
    super();
    this.dob = dob;
    this.name = name;
    this.surname = surname;
    this.ppsn = ppsn;
    this.address = address;
    this.phone = phone;
    this.email = email;
    this.password = password;
}
 public Long getId() {
    return id;
}

public String getDob() {
    return dob;
}

public String getName() {
    return name;
}

public String getSurname() {
    return surname;
}

public String getPpsn() {
    return ppsn;
}

public String getAddress() {
    return address;
}

public void setAddress(String address) {
    this.address = address;
}

public String getPhone() {
    return phone;
}

public void setPhone(String phone) {
    this.phone = phone;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

public String getNextApptId() {
    return nextApptId;
}

public void setNextApptId(String apptId) {
    this.nextApptId = apptId;
}

public String getDose1Date() {
    return dose1Date;
}

public void setDose1Date(String dose1Date) {
    this.dose1Date = dose1Date;
}

public String getDose2Date() {
    return dose2Date;
}

public void setDose2Date(String dose2Date) {
    this.dose2Date = dose2Date;
}

public String getLastLogin() {
    return lastLogin;
}

public void setLastLogin(String lastLogin) {
    this.lastLogin = lastLogin;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}
}

But for some reason I can't figure out, the first four fields - i.e. Dob (date of birth), Name, Surname, and PPSN, are producing a null error when instantiating the user object on the server side, e.g.:

     `Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 4 errors<EOL>
Field error in object 'user' on field 'dob': rejected value [null]; codes [NotBlank.user.dob,NotBlank.dob,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.dob,dob]; arguments []; default message [dob]]; default message [must not be blank] `.

The other four fields, Address, Phone, Email, and Password appear to be working just fine.

As far as I can make out, there it isn't a typo issue (forgive me if that ends up being the case). I have intercepted the Post request using Burp to check that the contents of the fields were making it out of the form and into the request, with the right names for the fields in my User class, and indeed they are all there as intended.

I imagine this means that the issue is coming from how the back end controller code is interpreting this post request based on the model, but I have no real idea of how or where to start. Here is the current controller:

@GetMapping("/register")
    public String startRegistration(Model model) {
        model.addAttribute("user", new User());
        return "register";
    }  

@PostMapping("/register_attempt")
    public String registerAttempt(@Valid User newUser) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encodedPassword = passwordEncoder.encode(newUser.getPassword());
        newUser.setPassword(encodedPassword);
        userRepository.save(newUser);
        return "registered_successfully";
    }

EDIT: Further debugging & clarification with issue persisting

Removing the @Valid annotation from the Post Mapping and using old fashioned print statements shows the same results - that the first four fields are null, for no obvious reason.

    @PostMapping("/register_attempt")
public String registerAttempt(@ModelAttribute("user") User newUser) {
    System.out.println("Saving user ");
    System.out.println("Name "   newUser.getName());
    System.out.println("Last Name "   newUser.getSurname());
    System.out.println("PPSN "  newUser.getPpsn());
    System.out.println("DOB "  newUser.getDob());
    System.out.println("email "  newUser.getEmail());
    System.out.println("address "  newUser.getAddress());
    System.out.println("encrypted password "  newUser.getPassword());
    System.out.println("Phone num "  newUser.getPhone());
    userRepository.save(newUser);
    System.out.println("User saved");
    return "registered_successfully";
}

Using the following dummy data:

The form Pre-submission

Which sends this Post Request to the back end:

Post Request as captured in burp after submission - all information is there as intended

Results in these print statements:

Saving user 
name null
last Name null
ppsn null
dob null
email [email protected]
address Here and there
password password
Phone num 987654321

And these error messages:

javax.validation.ConstraintViolationException: Validation failed for classes [app.model.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='must not be blank', propertyPath=surname, rootBeanClass=class app.model.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
    ConstraintViolationImpl{interpolatedMessage='must not be blank', propertyPath=dob, rootBeanClass=class app.model.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
    ConstraintViolationImpl{interpolatedMessage='must not be blank', propertyPath=name, rootBeanClass=class app.model.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
    ConstraintViolationImpl{interpolatedMessage='must not be blank', propertyPath=ppsn, rootBeanClass=class app.model.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
]

If you have any insight into why this might be happening, I would very much appreciate it.

CodePudding user response:

You have th:object="${user}" in your Thymeleaf template, so I have to assume that you @GetMapping method in your controller has added an instance of User to the Model using addAttribute.

In your @PostMapping, you should also use @ModelAttribute:

@PostMapping("/register_attempt")
public String registerAttempt(@Valid @ModelAttribute("user") User newUser) {
  ...

You also need to have setters for each field. I see that User has no setName(String name), no setDob(String dob), ...

Some other tips:

  1. Do not create BCryptPasswordEncoder instances in your controller method itself. When you use Spring Boot, this should be an application wide singleton (Called a bean in Spring lingo). Add a class to your application e.g. ReservationSystemApplicationConfiguration which declares this:
@Configuration
public class ReservationSystemApplicationConfiguration {
  @Bean
  public PasswordEncoder passwordEncoder() {
     return new BCryptPasswordEncoder();
  }
}

Then in your controller, inject the password encoder:

@Controller
@RequestMapping("...")
public class MyController {

  private final PasswordEncoder passwordEncoder;

  public MyController(PasswordEncoder passwordEncoder) {
    this.passwordEncoder = passwordEncoder;
  }

  @PostMapping("/register_attempt")
  public String registerAttempt(@Valid @ModelAttribute("user") User newUser) {
    String encodedPassword = passwordEncoder.encode(newUser.getPassword());
    newUser.setPassword(encodedPassword);
    userRepository.save(newUser);
    return "registered_successfully";
  }
}
  1. A Controller should not directly call the Repository, but use a Service in between.

  2. It is better to use different objects for mapping the form data and for storing the data into the database. See Form Handling with Thymeleaf for more information on how to do that.

  • Related