Backend Development 8 min read

Mastering Spring Formatter: Custom Data Conversion in Spring Boot 2.7

This tutorial explains Spring's core.convert package, introduces the Formatter interface, and provides step‑by‑step examples of creating custom formatters and annotation‑based formatters in Spring Boot, including code snippets, registration, and test endpoints with output screenshots.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Formatter: Custom Data Conversion in Spring Boot 2.7

Environment: Springboot 2.7.18

1. Introduction

Spring core.convert package is a generic type conversion system that provides a unified ConversionService API and a strong‑typed Converter SPI for converting between types. For localized string formatting, Spring introduced Formatter starting with version 3.0.

Formatter

Formatter implements field formatting logic and is strongly typed. Interface definition:

<code>public interface Formatter<T> extends Printer<T>, Parser<T> { }</code>

Formatter extends Printer and Parser . Their definitions are:

<code>// Convert from target type to String
public interface Printer<T> {
  String print(T fieldValue, Locale locale);
}
// Parse from String to target type
public interface Parser<T> {
  T parse(String clientValue, Locale locale) throws ParseException;
}</code>

To create a custom Formatter, implement the interface for the desired type.

2. Practical Examples

2.1 Custom Formatter

Parse an input like "张三,30" into a Users object.

<code>public class Users {
  private String name;
  private Integer age;
  // getters, setters
}</code>
<code>public class UsersFormatter implements Formatter<Users> {
  @Override
  public String print(Users object, Locale locale) {
    if (Objects.isNull(object)) {
      return "";
    }
    return "【name = " + object.getName() + ", age = " + object.getAge() + "】";
  }
  @Override
  public Users parse(String text, Locale locale) throws ParseException {
    if (text == null || text.trim().length() == 0) {
      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:

<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:

2.2 Annotation‑Based Formatter

Use AnnotationFormatterFactory to bind a custom annotation to a formatter.

<code>public interface AnnotationFormatterFactory<A extends Annotation> {
  Set<Class<?>> getFieldTypes();
  Printer<?> getPrinter(A annotation, Class<?> fieldType);
  Parser<?> getParser(A annotation, Class<?> fieldType);
}</code>

getFieldTypes: returns field types that can use the annotation.

getPrinter: returns a Printer for the annotated field.

getParser: returns a Parser for the annotated field.

Custom annotation:

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface AgeFormat {}
</code>

Formatter factory implementation:

<code>public final class AgeFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<AgeFormat> {
  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 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));
    }
  }
}</code>

Register the factory:

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

Apply the annotation to a 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:

Another example using a custom UsersFormat annotation follows the same steps, with endpoint:

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

Result:

backendSpringSpringBootannotationCustom ConverterFormatter
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.