Backend Development 8 min read

Master Advanced Data Binding in Spring Boot 3: From Simple Types to Custom Converters

This article demonstrates how to leverage Spring Boot 3's data binding capabilities by converting primitive types to objects, creating custom converters, handling hierarchical object binding, and implementing custom argument resolvers, complete with code examples and execution results.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Advanced Data Binding in Spring Boot 3: From Simple Types to Custom Converters

Environment: SpringBoot 3.4.2

1. Introduction

This article explains how to use Spring's data binding mechanism to automatically convert basic types to objects, improving code clarity and readability.

By default, Spring can bind simple types like int, String, or boolean to controller parameters, but more complex object binding requires custom solutions.

2. Practical Cases

2.1 Single Object Binding of Request Parameters

Define a controller that receives a LocalDateTime path variable, which fails without a proper converter:

<code>@RestController
@RequestMapping("/api")
public class ApiController {
  @GetMapping("/{date}")
  public ResponseEntity<?> findByDate(@PathVariable("date") LocalDateTime date) {
    return ResponseEntity.ok(date);
  }
}</code>

Solution: use @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") on the parameter or create a custom Converter :

<code>@Component
public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
  @Override
  public LocalDateTime convert(String source) {
    return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
  }
}</code>

Custom converters can also handle string‑to‑enum conversion:

<code>@GetMapping("/pay")
public ResponseEntity<Object> pay(Payment payment) {
  return ResponseEntity.ok(payment);
}
public enum Payment { ALI, WX; }</code>
<code>public class StringToEnumConverter implements Converter<String, Payment> {
  public Payment convert(String from) {
    return Payment.valueOf(from.toUpperCase());
  }
}</code>

Register the converter:

<code>@Component
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEnumConverter());
  }
}</code>

2.2 Hierarchical Object Binding

When binding an entire object tree, define a base class and a concrete subclass:

<code>public abstract class BaseEntity {
  private long id;
  public BaseEntity(long id) { this.id = id; }
  // getters & setters
}
public class User extends BaseEntity {
  private String name;
  public User(long id) { super(id); }
}</code>

Controller method:

<code>@GetMapping("/obj/{user}")
public ResponseEntity<Object> getStringToFoo(@PathVariable User user) {
  return ResponseEntity.ok(user);
}</code>

Create a ConverterFactory to handle different subclasses:

<code>public class StringToBaseEntityConverterFactory implements ConverterFactory<String, BaseEntity> {
  @Override
  public <T extends BaseEntity> Converter<String, T> getConverter(Class<T> targetClass) {
    return new StringToBaseEntityConverter<>(targetClass);
  }
  private static class StringToBaseEntityConverter<T extends BaseEntity> implements Converter<String, T> {
    private final Class<T> targetClass;
    public StringToBaseEntityConverter(Class<T> targetClass) { this.targetClass = targetClass; }
    @Override
    public T convert(String source) {
      String[] parts = source.split(",");
      long id = Long.parseLong(parts[0]);
      if (targetClass == User.class) {
        User user = new User(id);
        user.setName(parts.length > 1 ? parts[1] : null);
        return (T) user;
      }
      return null;
    }
  }
}</code>

Register the factory:

<code>@Override
public void addFormatters(FormatterRegistry registry) {
  registry.addConverterFactory(new StringToBaseEntityConverterFactory());
}</code>

2.3 Custom Argument Resolver for Binding

Define a custom annotation to bind a request header:

<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Version { }</code>

Implement the argument resolver:

<code>public class HeaderVersionArgumentResolver implements HandlerMethodArgumentResolver {
  @Override
  public boolean supportsParameter(MethodParameter methodParameter) {
    return methodParameter.getParameterAnnotation(Version.class) != null;
  }
  @Override
  public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
      NativeWebRequest nativeWebRequest, WebDataBinderFactory binderFactory) {
    HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();
    return request.getHeader("x-version");
  }
}</code>

Register the resolver:

<code>@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
  resolvers.add(new HeaderVersionArgumentResolver());
}</code>

Controller using the custom annotation:

<code>@GetMapping("/version/{id}")
public ResponseEntity<?> findByVersion(@PathVariable Long id, @Version String version) {
  return ResponseEntity.ok(id + "@" + version);
}</code>

Result can also be achieved with Spring MVC's built‑in @RequestHeader("x-version") annotation, but the custom resolver demonstrates extensibility.

Result image
Result image
JavaSpring BootData BindingCustom ConverterWeb MVC
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.