ConversionService aware Spring Custom Converter

Updated: Sep 14

The Data Transfer Object ( DTO ) pattern helps in wrapping the request body into a POJO. It is a good practice to keep the DB Model Entities separate from the DTOs although they end up having similar state fields. In this case the most common thing to do is creating a converter class which converts a DTO to ENTITY. Creating your own converter classes and then calling them on demand is a boilerplate code. Spring framework provides a Converter Interface which helps you get rid of the boilerplate code. A ConversionService helps in easily locating an appropriate converter as per your need. However, conversion of a nested DTOs is tricky since a converter is registered with ConversionService and if we try to Autowire the Conversion Service into another converter then it creates a circular dependency. We will understand this use case in this blog.



Example

Consider below simple example wherein you have a UserDto which you want to convert into an UserEntity.


public record UserDto(
        String name,
        AddressDto address
){}

public record AddressDto(
        String city,
        String state
) {}

public record UserEntity(
    String name,
    AddressEntity address
) { }

public record AddressEntity(
        String city,
        String state
) { }
        

Simple Converter using Converter Interface

A simple converter would be implementing the ConverterInterface as shown below



@Component
public class AddressDtoConverter implements Converter<AddressDto, AddressEntity> {

    @Override
    public AddressEntity convert(AddressDto source) {
        return new AddressEntity(source.city(), source.state());
    }
    
}

@Component
public class UserDtoConverter implements Converter<UserDto, UserEntity> {

    @Autowired
    private ConversionService conversionService;

    @Override
    public UserEntity convert(UserDto source) {
        var addressEntity = conversionService.convert(source.address(), AddressEntity.class);
        return new UserEntity(source.name(), addressEntity);
    }

}

Ideally we will need to Autowire the ConversionService into the UserDtoConverter to help locate the appropriate converter for AddressDto. You would think a simple way to use above converter is as shown below :


@Service
public class UserService {

    @Autowired
    private ConversionService conversionService;

    public void save(UserDto userDto){
        var mappedEntity = conversionService.convert(userDto, UserEntity.class);
    }

}

But if we Autowire ConversionService into UserDtoConverter we get below error :


***************************
APPLICATION FAILED TO START
***************************

Description:

Field conversionService in com.example.demo.converters.UserDtoConverter required a bean of type 'org.springframework.core.convert.ConversionService' that could not be found.

The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)



Auto Registering Converter

A converter which registers itself with ConversionService


To resolve this issue we need to create an abstract AutoRegisteringConverter which registers itself with the ConversionService post its own construction. Below is the required code for that :


Note : Below sample is suggested by André Wolf in this GitHub discussion


public abstract class AutoRegisteringConverter<S, T> implements Converter<S, T> {

    private ConversionService conversionService;

    public ConversionService getConversionService() {
        return conversionService;
    }

    @Autowired
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @PostConstruct
    private void register() {
        if (conversionService instanceof GenericConversionService) {
            ((GenericConversionService) conversionService).addConverter(this);
        }
    }
} 
 

If you want such a setup then you don't need to implement the Converter Interface for that converter instead extend above AutoRegisteringConverter as shown below :


@Component
public class UserDtoConverter extends AutoRegisteringConverter<UserDto, UserEntity> {

    @Override
    public UserEntity convert(UserDto source) {
        var addressEntity = getConversionService().convert(source.address(), AddressEntity.class);
        return new UserEntity(source.name(), addressEntity);
    }

}

Please note for simple converters which do not need to delegate the mapping to other converters should be still implementing the Converter interface as in our case the AddressDtoConverter.


Summary

We learnt how to ensure that a converter follows a single responsibility principle of converting only the DTOs which it is supposed to and delegate the work to the other Converters.