This is a continuing of the previous reading about the core functionality of the Spring Framework provided for types conversion. Here we’ll take a look at the concepts of Converters and Formatters.

lets-continue

Short reminding - the project serving as a victim for the conversion executions could be found here.

Converter<,>

The Converter concept is a more general one, it allows you to convert data between any two types. This means that you can use not only for the web-layer for converting from String but some more general conversion logic. Let’s start with this type-to-type example…

ConversionService direct usage

First, we would need the target property to be converted, let’s use for it the enum BookStatus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
enum BookStatus {
    ISSUED, FORBIDDEN, REISSUED
}

@ToString
class Book {
    int id
    Date issueDate
    Author author
    BookStatus status
}

As you can see it’s already part of our data model - Book. Every registered converter could be used by means of Spring’s ConversionService bean. I will write the code without the converter yet and run it to be sure, that we have a problem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@SpringBootApplication
class DemoApplication implements CommandLineRunner {

	@Autowired
	ConversionService conversionService

	static void main(String[] args) {
		SpringApplication.run(DemoApplication, args)
	}

	@Override
	void run(String... args) throws Exception {

		// Demonstrating that the conversion service could be used programmatically
		// even being configured in WebMvcConfigurer
		def status = conversionService.convert(1L, BookStatus) // Should be FORBIDDEN
		log.info "Runner... status after conversion: $status"
	}
}

when running, in the log we can see this:

1
No converter found capable of converting from type [java.lang.Long] to type [com.example.demo.model.BookStatus]

I have tried to convert Long to BookStatus. I used the Long instead of Integer for example because Spring has already predefined some converters implementation and IntegerToEnum is one of them.

Well, let’s fix the problem and create the converter from Long:

1
2
3
4
5
6
7
8
@Slf4j
class LongBookStatusConverter implements Converter<Long, BookStatus> {
    @Override
    BookStatus convert(Long source) {
        log.info "Converting $source"
        BookStatus.values().find {it.ordinal().toLong() == source}
    }
}

Registering

Now, we need to register our converter somehow. We can either create the bean of ConverstionService by means of ConverstionServiceFactoryBean or directly instantiating the @Bean of that type.

Also, Spring boot registers beans implementing Converter automatically. Let’s use this option, for the converter which we created. Adding @Component to the converter and running the application once again:

1
2
c.e.d.c.LongBookStatusConverter          : Converting 1
com.example.demo.DemoApplication         : Runner... status after conversion: FORBIDDEN

Formatter

Ok, that was nice. Now let’s talk a bit about the last but not least option for conversion - Formatter. As it’s written in the documentation, this concept is dedicated to the conversion in client-facing interfaces (like our Web app, for example). It can be used for converting from String and use the Locale of the client to provide integration with i18n.

In our Book model we have an issuedDate of Date type, so if we send a request like this:

1
2
3
4
5
curl --location --request POST 'localhost:8099/book' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'id=123' \
--data-urlencode 'author=Alex' \
--data-urlencode 'issueDate=1592009798000'

The server will throw an exception into us:

1
Field error in object 'book' on field 'issueDate': rejected value [1592009798000]

Well, let’s fix it using Formatter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Slf4j
class IssuedDateFormatter implements Formatter<Date> {
    @Override
    Date parse(String text, Locale locale) throws ParseException {
        log.info "Parsing date from $text"
        new Date(text.toLong())
    }

    @Override
    String print(Date dt, Locale locale) {
        log.info "printing date from $dt"
        dt.time.toString()
    }
}

It’s easy to notice here, that for parsing and printing you can use the Locale object, which makes it possible to provide i18n service. Besides, there is only one generic type being accepted - the type of target class.

Registering

The registration could be done by means of : * just creating a bean from formatter class, if it’s a boot-application; * creating FormattingConversionService directly or; * using FormattingConversionServiceFactoryBean, which in the end is the same; * directly in controller binder method (@InitBinder and WebDataBinder); * using WebMvcConfigurer, which is broadly being used to configure MVC-specific stuff.

So many options, you can choose whatever fits better for your case. For example, I’ll use the last one, but with small improvement - I’ll show how registrar concept could be used in case of formatters, quite similar to what we had for editors in the previous reading.

Registrar with our formatter:

1
2
3
4
5
6
class BookDatesFormattersRegistrar implements FormatterRegistrar {
    @Override
    void registerFormatters(FormatterRegistry registry) {
        registry.addFormatter(new IssuedDateFormatter())
    }
}

Registration itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
class WebConfig implements WebMvcConfigurer {
    @Override
    void addFormatters(FormatterRegistry registry) {
        bookDatesFormattersRegistrar().registerFormatters(registry)
    }

    @Bean
    BookDatesFormattersRegistrar bookDatesFormattersRegistrar() {
        new BookDatesFormattersRegistrar()
    }
}

Let’s run one more time the POST request with issuedDate, and we will see:

1
2
c.e.demo.formatters.IssuedDateFormatter  : Parsing date from 1592009798000 
c.e.demo.controllers.GeneralController   : Created book: com.example.demo.model.Book(123, Sat Jun 13 02:56:38 CEST 2020, com.example.demo.model.Author(Alex), null)

Annotation-driven formatting

Finishing the topic with formatters, I should mention the subclass of it - AnnotationFormatterFactory This is a very convenient way of applying conversion according to the custom annotation, which could be put just right on top of the field. This approach is very explicit and allows us to see the conversion configuration in place - in your model data.

Besides, Spring has a few predefined annotation-based formatter, which you probably saw before.

An example from the documentation:

1
2
3
4
5
public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}

Unfortunately, this theme is quite wide and a bit more in details of one particular option of conversion types, so I’ll just mention it and will not cover it in more detail.

Conclusion

These type conversion concepts are basic things in Spring, but they lay down in the foundation of many other abstractions and services, which people get from Spring. Thus, it’s quite important to understand them well and be able to use them. I hope this reading helped to build a more structured picture of Spring type conversion option for you.