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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.