Code is extracted from my notice board example application.
Thymeleaf template of input form 'resources/templates/private/message.html'
<!DOCTYPE html>
<html th:lang="${#locale.language}" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
  <title>[[#{applicationName}]] - [[${isEdit}? #{editMessage}: #{newMessage}]]</title>
  <meta charset="utf-8">
  <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
  <h4 th:text="${isEdit}? #{editMessage}: #{newMessage}"></h4>
  <form th:action="@{/manage/__${postHandler}__}" method="post" th:object="${message}" class="ui form">
    <fieldset>
      <legend>[[#{message}]]</legend>
      <div class="field">
        <label>[[#{message.publishDate}]]</label>
        <input type="text" th:field="*{publishDate}" />
      </div>
      <div class="field">
        <label>[[#{message.removeDate}]]</label>
        <input type="text" th:field="*{removeDate}" />
      </div>
      <div class="field">
        <label>[[#{message.description}]]</label>
        <textarea name="description" th:text="*{description}"></textarea>
      </div>
      <button class="ui mini primary button">[[#{save}]] <i class="send icon"></i></button>
    </fieldset>
  </form>
</div>
</body>
</html>
Actual HTML code received by browner when calling http://localhost:8080/manage/new
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Notice board - New message</title>
  <meta charset="utf-8">
  <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
  <h4>New message</h4>
  <form action="/manage/new/save" method="post" class="ui form"><input type="hidden" name="_csrf" value="87baeee6-deea-4c4b-8b2b-b17e9be876e0"/>
    <fieldset>
      <legend>Message</legend>
      <div class="field">
        <label>Publish Date</label>
        <input type="text" id="publishDate" name="publishDate" value="" />
      </div>
      <div class="field">
        <label>Remove Date</label>
        <input type="text" id="removeDate" name="removeDate" value="" />
      </div>
      <div class="field">
        <label>Message</label>
        <textarea name="description"></textarea>
      </div>
      <button class="ui mini primary button">Save <i class="send icon"></i></button>
    </fieldset>
  </form>
</div>
</body>
</html>
After filling data like below
click 'Save', a HTTP POST request with following parameter sends to http://localhost:8080/manage/new/save
saveCreateMessage() method in info.saladlam.example.spring.noticeboard.controller.PrivateController responses for handle this request
@Controller
@RequestMapping("/manage")
public class PrivateController {
	@PostMapping("/new/save")
	public String saveCreateMessage(@ModelAttribute MessageDto message, BindingResult errors) {
		message.setOwner(this.getLoginName());
		this.messageService.save(message);
		return "redirect:/manage";
	}
	// ...
}
MessageDto instance is built by class org.springframework.web.method.annotation.ModelAttributeMethodProcessor, actual building operation is defined on method resolveArgument()
	@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
		Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
		String name = ModelFactory.getNameForParameter(parameter);
		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
		if (ann != null) {
			mavContainer.setBinding(name, ann.binding());
		}
		Object attribute = null;
		BindingResult bindingResult = null;
		if (mavContainer.containsAttribute(name)) {
			attribute = mavContainer.getModel().get(name);
		}
		else {
			// Create attribute instance
			try {
				attribute = createAttribute(name, parameter, binderFactory, webRequest);
			}
			catch (BindException ex) {
				if (isBindExceptionRequired(parameter)) {
					// No BindingResult parameter -> fail with BindException
					throw ex;
				}
				// Otherwise, expose null/empty value and associated BindingResult
				if (parameter.getParameterType() == Optional.class) {
					attribute = Optional.empty();
				}
				bindingResult = ex.getBindingResult();
			}
		}
		if (bindingResult == null) {
			// Bean property binding and validation;
			// skipped in case of binding failure on construction.
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					bindRequestParameters(binder, webRequest);
				}
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
			// Value type adaptation, also covering java.util.Optional
			if (!parameter.getParameterType().isInstance(attribute)) {
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
			}
			bindingResult = binder.getBindingResult();
		}
		// Add resolved attribute and BindingResult at the end of the model
		Map<String, Object> bindingResultModel = bindingResult.getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);
		return attribute;
	}
MessageDto instance obtains after running
bindRequestParameters(binder, webRequest);
and validation performs if validator is defined
validateIfApplicable(binder, parameter);
finally, MessageDto instance as parameter message is passed into controller

