Spring Type Conversion and Field Formatting – writing the first converter or formatter

Preamble

In the article, I would like to consider writing your own Spring Framework type converters and field formatters (including using annotations).

The article was written by junior for junior, so please treat the following with a fair amount of indulgence šŸ™‚

Converter – converts one data type to another
Formatter – convert only type String to some other type

Nice explanation from stackoverflow

We will consider the work of converters and formatters using a completely banal example – a string with a date or time comes to @RequestParam of the controller and you need to convert it to LocalDate or LocalTime. In place of the string with the date / time, there can be anything, for example, a description of the database entity. But I think this would complicate the example, so for simplicity, let the date and time remain.

@RestController  
@RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE)  
public class SomeRestController {  

	// autowired services and others
	// ...
	
    @Override  
    @GetMapping("/filter")  
    public List<SomeDto> getFiltered(  
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,  
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime startTime,  
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,  
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime endTime) {  
  
        // ...
    }  
}

In the implementation above, everything is converted using Spring Framework’s formatting annotations. For practice, we will abandon the use of standard annotations and write our own converters, then formatters and annotations.

I will specify the configuration only in xml, as the most muddy.

Converter

Documentation

In the simplest case, to create your own converter, you need to create a class that implements the Converter interface. S – data source type, T – data type to be obtained as a result of conversion.

In our case, the source is a String type, and the resulting types are LocalDate and LocalTime. Thus, we will have two converter classes. Implementation option:

import org.springframework.core.convert.converter.Converter;  
  
import java.time.LocalDate;  
import java.time.format.DateTimeFormatter;  
  
public class StringToLocalDateConverter implements Converter<String, LocalDate> {  
  
    private String datePattern = "yyyy-MM-dd";  
  
    public String getDatePattern() {  
        return datePattern;  
    }  
  
    public void setDatePattern(String datePattern) {  
        this.datePattern = datePattern;  
    }  
  
    @Override  
    public LocalDate convert(String dateString) {  
        return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(datePattern));  
    }  
}
import org.springframework.core.convert.converter.Converter;  
  
import java.time.LocalTime;  
import java.time.format.DateTimeFormatter;  
  
public class StringToLocalTimeConverter implements Converter<String, LocalTime> {  
  
    private String timePattern = "HH:mm";  
  
    public String getTimePattern() {  
        return timePattern;  
    }  
  
    public void setTimePattern(String timePattern) {  
        this.timePattern = timePattern;  
    }  
  
    @Override  
    public LocalTime convert(String timeString) {  
        return LocalTime.parse(timeString, DateTimeFormatter.ofPattern(timePattern));  
    }  
}

Now we need to tell Spring about our converters. To do this, we need to create converter beans, register an instance of the ConversionService class with the name conversionService and add our converters to it.

Documentation

<bean id="stringToLocalTimeConverter" class="ru.jsft.util.converter.StringToLocalTimeConverter"/>  
<bean id="stringToLocalDateConverter" class="ru.jsft.util.converter.StringToLocalDateConverter"/>

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">  
    <property name="converters">
        <set>
            <ref bean="stringToLocalDateConverter"/>
            <ref bean="stringToLocalTimeConverter"/>  
        </set>
    </property>
</bean>

The final touch is to tell Spring MVC to use our ConversionService

Documentation

mvc:annotation-driven xml-configuration must be supplemented with the conversionService indication

<mvc:annotation-driven conversion-service="conversionService"/>

Now you can rewrite the controller method

@RestController  
@RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE)  
public class SomeRestController {  

	// autowired services and others
	// ...
	
    @Override  
    @GetMapping("/filter")  
    public List<SomeDto> getFiltered(  
            @RequestParam(required = false) LocalDate startDate,  
            @RequestParam(required = false) LocalTime startTime,  
            @RequestParam(required = false) LocalDate endDate,  
            @RequestParam(required = false) LocalTime endTime) {  
  
        // ...
    }  
}

Abstract

  1. Created converter classes that implement Converter

  2. Created class beans (via xml, annotation or configuration class)

  3. We created a conversionService bean through the configuration and specified our converters in it

  4. Tell Spring MVC to use our conversionService

Formatter

Now let’s do the same, only through string formatting.

Documentation

It is necessary to create a class that implements the Formatter< T > interface, where T is the type of data that will be obtained as a result of formatting the input string. Let me remind you that the formatter only works with String, so the input data type is not needed.

Implementation option

import org.springframework.format.Formatter;  
  
import java.time.LocalDate;  
import java.time.format.DateTimeFormatter;  
import java.util.Locale;  
  
public class CustomDateFormatter implements Formatter<LocalDate> {  
    @Override  
    public LocalDate parse(String text, Locale locale) {  
        return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));  
    }  
  
    @Override  
    public String print(LocalDate localDate, Locale locale) {  
        return localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd");  
    }  
}
import org.springframework.format.Formatter;  
  
import java.time.LocalTime;  
import java.time.format.DateTimeFormatter;  
import java.util.Locale;  
  
public class CustomTimeFormatter implements Formatter<LocalTime> {  
    @Override  
    public LocalTime parse(String text, Locale locale) {  
        return LocalTime.parse(text, DateTimeFormatter.ofPattern("HH:mm"));  
    }  
  
    @Override  
    public String print(LocalTime localTime, Locale locale) {  
        return localTime.format(DateTimeFormatter.ofPattern("HH:mm"));  
    }  
}

Let’s tell Spring about formatters. Let’s tell Spring MVC to use our conversionService.

Documentation

Note that the conversionService bean uses a different class than the one used for the converters. When using this class, by the way, you can add converters in addition to formatters. See documentation.

<bean id="customDateFormatter" class="ru.jsft.util.formatter.CustomDateFormatter"/>  
<bean id="customTimeFormatter" class="ru.jsft.util.formatter.CustomTimeFormatter"/>

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">  
	<property name="formatters">  
		<set>
			<ref bean="customDateFormatter"/>  
			<ref bean="customTimeFormatter"/>  
		</set>        
	</property>    
</bean>  

<mvc:annotation-driven conversion-service="conversionService"/>

The controller method will look the same as in the case of using converters

@RestController  
@RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE)  
public class SomeRestController {  

	// autowired services and others
	// ...
	
    @Override  
    @GetMapping("/filter")  
    public List<SomeDto> getFiltered(  
            @RequestParam(required = false) LocalDate startDate,  
            @RequestParam(required = false) LocalTime startTime,  
            @RequestParam(required = false) LocalDate endDate,  
            @RequestParam(required = false) LocalTime endTime) {  
  
        // ...
    }  
}

Abstract

  1. Created formatter classes that implement Formatter< T >

  2. Created class beans (via xml, annotation or configuration class)

  3. We created a conversionService bean through the configuration and specified our formatters in it

  4. Tell Spring MVC to use our conversionService

Formatting with Annotations

Now let’s make it so that the formatting happens only where we specify the appropriate annotations. In the case of the previous implementation options, the use of converters / controllers occurs throughout the code.

I want the parameter annotation to be able to be applied to both convert to LocalDate and LocalTime. That is, do not make two different annotations, but make one, but with a refinement parameter (as it is implemented in the case @DateTimeFormat(iso = DateTimeFormat.ISO.DATE))

Documentation

First of all, let’s create an interface for the new annotation. In the interface, we will declare a parameter that will store an indication of what to convert to – in LocalDate or in LocalTime. As the type of the parameter, we specify the enum declared right there. I didn’t set the default value for the parameter.

import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
  
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface CustomDateTimeFormat {  
  
    Type type();  
  
    public enum Type {  
        DATE,  
        TIME  
    }  
}

Now we need to bind the annotation to the formatter. This is done by implementing a class that implements the AnnotationFormatterFactory< A extends Annotation > interface.

import org.springframework.format.AnnotationFormatterFactory;  
import org.springframework.format.Formatter;  
import org.springframework.format.Parser;  
import org.springframework.format.Printer;  
  
import java.time.LocalDate;  
import java.time.LocalTime;  
import java.util.HashSet;  
import java.util.List;  
import java.util.Set;  
  
public class CustomDateTimeFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<CustomDateTimeFormat> {  
  
    @Override  
    public Set<Class<?>> getFieldTypes() {  
        return new HashSet<>(List.of(LocalDate.class, LocalTime.class));  
    }  
  
    @Override  
    public Printer<?> getPrinter(CustomDateTimeFormat annotation, Class<?> fieldType) {  
        return getFormatter(annotation, fieldType);  
    }  
  
    @Override  
    public Parser<?> getParser(CustomDateTimeFormat annotation, Class<?> fieldType) {  
        return getFormatter(annotation, fieldType);  
    }  
  
    private Formatter<?> getFormatter(CustomDateTimeFormat annotation, Class<?> fieldType) {  
        switch (annotation.type()) {  
            case DATE -> {  
                return new CustomDateFormatter();  
            }  
            case TIME -> {  
                return new CustomTimeFormatter();  
            }  
        }  
        return null;  
    }  
}

Here, let’s take a closer look at the overridden methods of the AnnotationFormatterFactory interface.

getFieldType() – the method returns a list of data type classes with which the annotation will be used. Note that if you annotate a type that is not in this list, then nothing will happen despite the presence of the annotation.

getPrinter() and getParser() – the first returns Printer to display the value of the annotated field, the second returns Parser to parse the received value. In both cases, our code will be the same. The idea is that if the annotation has the type == DATE parameter, then the instance of the CustomDateFormatter class already written by us will be returned. And for type == TIME, an instance of CustomTimeFormatter will be returned accordingly. Thus, we achieve that the annotation is one, and the returned result is different.

Well, now it remains to introduce Spring to our annotation binding. Don’t forget to point Spring MVC to our conversionService.

Documentation

<bean id="customDateTimeFormatAnnotationFormatterFactory" class="ru.jsft.util.formatter.CustomDateTimeFormatAnnotationFormatterFactory"/>

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">  
	<property name="formatters">  
		<set>
			<ref bean="customDateTimeFormatAnnotationFormatterFactory"/>  
		</set>        
	</property>   
</bean>

<mvc:annotation-driven conversion-service="conversionService"/>

Now let’s change the controller method so that the incoming parameters are formatted using annotations

@RestController  
@RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE)  
public class SomeRestController {  

	// autowired services and others
	// ...
	
    @Override  
    @GetMapping("/filter")  
    public List<SomeDto> getFiltered(  
            @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.DATE) LocalDate startDate,  
            @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.TIME) LocalTime startTime,  
            @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.DATE) LocalDate endDate,  
            @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.TIME) LocalTime endTime) {  
  
        // ...
    }  
}

Abstract

  1. We already had formatter classes

  2. Created interface annotation

  3. Created an annotation binding class to the formatter

  4. We created a conversionService bean through the configuration and specified in it a class that binds annotations to formatters

  5. Tell Spring MVC to use our conversionService

Conclusion

I sincerely hope that this article will be useful to those who are just starting to deal with the topic of type conversion and field formatting using the Spring Framework. I understand that a significant part of the information on these topics has not been sorted out. But it wasn’t my intention to translate the documentation for Spring or create some sort of comprehensive guide. This article is just a way to help get the ball rolling.

Thank you for reading to the end.

Similar Posts

Leave a Reply Cancel reply