Backend Development 7 min read

Master Custom Data Type Conversion in Spring Boot with Formatter SPI

This guide explains how to use Spring Boot's Formatter SPI to create custom type converters, implement both simple and annotation‑based formatters, register them in a WebMvcConfigurer, and test the conversions through REST endpoints, complete with code examples.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Custom Data Type Conversion in Spring Boot with Formatter SPI

Environment

Spring Boot 2.6.12

Conversion Service and Formatter SPI

Spring provides two SPI mechanisms for type conversion: the Converter SPI for generic conversions (e.g., java.util.Date ↔ Long ) and the Formatter SPI for locale‑aware formatting in web applications. ConversionService offers a unified API for both.

Creating a Custom Formatter

Implement the Formatter<T> interface (which extends Printer<T> and Parser<T> ) for the target type, for example Users :

<code>public class Users {
    private String name;
    private Integer age;
}</code>
<code>public class UsersFormatter implements Formatter<Users> {
    @Override
    public String print(Users object, Locale locale) {
        if (object == null) {
            return "";
        }
        return "【name = " + object.getName() + ", age = " + object.getAge() + "】";
    }

    @Override
    public Users parse(String text, Locale locale) throws ParseException {
        if (text == null || text.trim().isEmpty()) {
            return null;
        }
        Users user = new Users();
        String[] values = text.split(",");
        user.setName(values[0]);
        user.setAge(Integer.parseInt(values[1]));
        return user;
    }
}</code>

Register the formatter in a @Configuration class that implements WebMvcConfigurer :

<code>@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new UsersFormatter());
    }
}</code>

Test endpoint:

<code>@GetMapping("/save")
public Object save(Users users) {
    return users;
}</code>

Result (example output):

Annotation‑Based Formatter

Define a custom annotation and an AnnotationFormatterFactory to bind the annotation to a formatter.

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface AgeFormat {}
</code>
<code>public final class AgeFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<AgeFormat> {
    @Override
    public Set<Class<?>> getFieldTypes() {
        Set<Class<?>> types = new HashSet<>();
        types.add(Integer.class);
        return types;
    }

    @Override
    public Printer<Integer> getPrinter(AgeFormat annotation, Class<?> fieldType) {
        return new AgeFormatter();
    }

    @Override
    public Parser<Integer> getParser(AgeFormat annotation, Class<?> fieldType) {
        return new AgeFormatter();
    }

    private static class AgeFormatter implements Formatter<Integer> {
        @Override
        public String print(Integer object, Locale locale) {
            return object == null ? "" : object.toString();
        }

        @Override
        public Integer parse(String text, Locale locale) throws ParseException {
            if (text == null || text.trim().isEmpty()) {
                return -1;
            }
            return Integer.parseInt(text.substring(1)); // skip leading 's'
        }
    }
}
</code>

Register the annotation formatter:

<code>@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatterForFieldAnnotation(new AgeFormatAnnotationFormatterFactory());
    }
}
</code>

Apply the annotation to the age field:

<code>public class Users {
    private String name;
    @AgeFormat
    private Integer age;
}
</code>

Test endpoint:

<code>@GetMapping("/save2")
public Object save2(Users users) {
    return users;
}
</code>

Result (example output):

Formatter on Method Parameter

Another custom annotation can be used on a controller method parameter:

<code>public final class UsersFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<UsersFormat> {
    @Override
    public Set<Class<?>> getFieldTypes() {
        Set<Class<?>> types = new HashSet<>();
        types.add(Users.class);
        return types;
    }
    @Override
    public Printer<?> getPrinter(UsersFormat annotation, Class<?> fieldType) {
        return new UsersFormatter();
    }
    @Override
    public Parser<?> getParser(UsersFormat annotation, Class<?> fieldType) {
        return new UsersFormatter();
    }
}
</code>
<code>@GetMapping("/save3")
public Object save3(@UsersFormat Users users) {
    return users;
}
</code>

Result (example output):

JavaSpring BootannotationCustom ConverterFormatter SPI
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.