Loading...

Spring MVC: How to Use a Custom Converter for Type Safety in Controllers

Spring
August 23, 2023
3 minutes to read
Share this post:

Are your controllers constantly filled with primitives? In ArticleController, the first line of every method is: articleRepository.findById(articleId). We keep making the same conversions. I hardly observe as many primitives in any other layer as in the API layer. This leads to much boilerplate. The code becomes harder to test. And it’s harder to read. Yet we know how much easier it is when we work with type safety. In this article, I will show you how to use the Converter Interface to automatically convert your primitives into the correct type.

A Converter in Spring is a simple interface that defines a transformation between types. The interface looks like this:

@FunctionalInterface
public interface Converter<S, T> {

	/**
	 * Convert the source object of type {@code S} to target type {@code T}.
	 * @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
	 * @return the converted object, which must be an instance of {@code T} (potentially {@code null})
	 * @throws IllegalArgumentException if the source cannot be converted to the desired target type
	 */
	@Nullable
	T convert(S source);

}

A Converter Ensures Type Safety

Type safety makes your code more robust and less prone to errors. By implementing a converter, you can ensure that the data flowing through the various layers of your application always conforms to the expected types. This not only contributes to code quality but also to the maintainability and understandability of the code.

For example, a Converter can be used in combination with @PathVariable

Suppose you have an API that provides details of an article under GET /api/v2/article/myArticleId. With @PathVariable, you can access myArticleId:

@RestController
@RequestMapping("/api/v2/article")
public class UserController {

    private final ArticleRepository articleRepository;
    
    public UserController(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }

    @GetMapping("/{myArticleId}")
    public ArticleDTO getArticle(@PathVariable long myArticleId) {
        Article article = articleRepository.findById(myArticleId);
        return ArticleDTO.from(article);
    }
}

In this implementation, we will keep making the same conversion over and over again. In all subsequent operations, we first call articleRepository#findById. While this isn’t really a problem in this small example, it becomes one as soon as our API becomes more complex.

With a Custom Converter, We Can Reduce This Complexity

And that’s not complicated at all. The following implementation is sufficient to centralize the above conversion:

@Component
public class LongToArticleConverter implements Converter<Long, Article> {

    private final ArticleRepository articleRepository;

    public LongToArticleConverter(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }
    
    @Override
    public Article convert(Long articleId) {
        return articleRepository.findById(articleId);
    }
}

By using @Component, we have automatically made the converter known to the context.

Now You Can Use Article Directly in the Controller

The hard part is already done. We can replace our implementation above with the following:

@RestController
@RequestMapping("/api/v2/article")
public class UserController {

    @GetMapping("/{myArticleId}")
    public ArticleDTO getArticle(@PathVariable Article article) {
        return ArticleDTO.from(article);
    }
}

Isn’t that much simpler? Our controller clearly describes its purpose. And this method is not only easy to read. It’s also easy to test. You can now write a unit test that doesn’t require a mock. We have written clean, modular, and easy-to-understand code.

Have you heard of Marcus' Backend Newsletter?

New ideas. Twice a week!
Top