Mastering Spring’s Type Conversion: Services, Custom Converters, and Best Practices
This article explains Spring’s type conversion system, introduces the core conversion service interfaces, demonstrates how to use GenericConversionService, DefaultConversionService, and WebConversionService, and provides step‑by‑step examples of custom converters, factories, generic converters, and their registration in Spring MVC.
Environment: Spring 5.3.23
Overview
Spring’s data type conversion makes code more elegant and efficient. The framework offers many built‑in converters that simplify converting data between types, reducing complexity while improving readability and maintainability.
In Spring you can use Java generics and conversion utilities to cast or transform values, and you can register custom converters via the Converter interface or related registration classes.
The Formatter interface lets you define custom formatters to format data and parse it back to the original type.
Since Spring 3, the core.convert package provides a generic conversion system with an SPI for conversion logic and an API for runtime conversion. It can replace PropertyEditor in the container and be used anywhere a conversion is needed.
1. Type Conversion Service
ConversionService interface
<code>public interface ConversionService {
// can we convert?
boolean canConvert(Class<?> sourceType, Class<?> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
// perform conversion
<T> T convert(Object source, Class<T> targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}</code>In most cases you should implement ConfigurableConversionService , which extends ConversionService and ConverterRegistry to add or remove specific converters.
ConfigurableConversionService interface
<code>public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {}</code>Spring provides GenericConversionService as a ready‑to‑use implementation.
<code>GenericConversionService gcs = new GenericConversionService();
Long result = gcs.convert("10", Long.class);
System.out.println(result);
</code>The above code throws ConverterNotFoundException because no converter is registered:
<code>Exception in thread "main" org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [java.lang.Long]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322)
</code>FormattingConversionService extends GenericConversionService and adds support for Printer and Parser :
<code>FormattingConversionService fcs = new FormattingConversionService();
fcs.addParser(new Parser<Teacher>() {
@Override
public Teacher parse(String text, Locale locale) throws ParseException {
String[] t = text.split("\\|");
return new Teacher(t[0], Integer.valueOf(t[1]));
}
});
System.out.println(fcs.convert("张晶晶|26", Teacher.class));
</code>Similarly, a custom Printer can turn an object into a readable string:
<code>FormattingConversionService fcs = new FormattingConversionService();
fcs.addPrinter(new Printer<Teacher>() {
@Override
public String print(Teacher object, Locale locale) {
return "【 name = " + object.getName() + ", age = " + object.getAge() + "】";
}
});
System.out.println(fcs.convert(new Teacher("张晶晶", 26), String.class));
</code>Spring also supplies DefaultConversionService and WebConversionService . The former registers many common converters out of the box:
<code>DefaultConversionService dcs = new DefaultConversionService();
Long result = dcs.convert("10", Long.class);
Date date = dcs.convert("2022-07-01", Date.class);
System.out.println(result);
System.out.println(date);
</code>The source code of DefaultConversionService shows it adds scalar and collection converters automatically.
<code>public class DefaultConversionService extends GenericConversionService {
public DefaultConversionService() {
addDefaultConverters(this);
}
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
// ... many more converters
}
}
</code>In a web environment Spring Boot uses WebConversionService (a subclass of DefaultFormattingConversionService ) which is configured via WebMvcAutoConfiguration :
<code>public class WebMvcAutoConfiguration {
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
@Bean
@Override
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters()
.dateFormat(format.getDate())
.timeFormat(format.getTime())
.dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
}
}
</code>Inheritance hierarchy:
<code>public class WebConversionService extends DefaultFormattingConversionService {}
public class DefaultFormattingConversionService extends FormattingConversionService {}
public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware {}
</code>Spring also provides ConversionServiceFactoryBean to register custom converters as a bean:
<code>public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {
private Set<?> converters;
private GenericConversionService conversionService;
public void setConverters(Set<?> converters) { this.converters = converters; }
// ... other methods
}
</code> <code>@Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean();
factory.setConverters(...); // custom converters
return factory;
}
</code>2. Custom Conversion Interfaces
Method 1: Implement Converter interface
<code>@FunctionalInterface
public interface Converter<S, T> {
T convert(S source);
}
</code> <code>DefaultConversionService cs = new DefaultConversionService();
cs.addConverter(new Converter<String, Users>() {
public Users convert(String source) {
String[] temp = source.split("\\|");
return new Users(temp[0], Integer.parseInt(temp[1]));
}
});
Users users = cs.convert("张三|100", Users.class);
System.out.println(users);
</code>Method 2: Implement ConverterFactory interface
<code>public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
</code> <code>public class EnvObjectConvert implements ConverterFactory<String, EnvObject> {
@Override
public <T extends EnvObject> Converter<String, T> getConverter(Class<T> targetType) {
return new EnvConvert<>();
}
private class EnvConvert<T extends EnvObject> implements Converter<String, T> {
@Override
public T convert(String source) {
String[] temp = source.split("\\|");
return (T) new EnvObject(temp[0], Integer.valueOf(temp[1]));
}
}
}
</code>Method 3: Implement GenericConverter interface
<code>public interface GenericConverter {
Set<ConvertiblePair> getConvertibleTypes();
Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
</code> <code>public class CustomGenericConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> pairs = new HashSet<>();
pairs.add(new ConvertiblePair(String.class, Teacher.class));
pairs.add(new ConvertiblePair(String.class, Student.class));
return pairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String str = (String) source;
if (targetType.getObjectType() == Teacher.class) {
String[] t = str.split("\\|");
return new Teacher(t[0], Integer.valueOf(t[1]));
}
if (targetType.getObjectType() == Student.class) {
String[] t = str.split("\\|");
return new Student(t[0], t[1]);
}
return null;
}
}
</code>3. Register Custom Converters in Spring MVC
Method 1: Implement WebMvcConfigurer
<code>@Component
public class PackWebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, User>() {
// ...
});
}
}
</code>Method 2: Use @InitBinder in a controller
<code>@RestController
@RequestMapping("/users")
public class UserController {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(null);
binder.registerCustomEditor(null, null);
}
}
</code>Note: This approach only applies to the current controller.
Method 3: Global @InitBinder via @RestControllerAdvice
<code>@RestControllerAdvice
public class GlobalAdviceController {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(null);
binder.registerCustomEditor(null, null);
}
}
</code>This makes the binder available to all controller methods.
Finished!!!
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.