Search code examples
mongodbspring-bootserializationjackson

How do I save a LocalDate in my MongoDB database as a String without changing the class model?


Technologies

  • Java 17
  • Springboot 3.2
  • MongoDB

Contract Model

I'm using a Model called Contract that has an attribute called startDate which is a LocalDate.

@Data
@Document("contract")
public class Contract {

// ...

  /* shows the start date of the contract */
  @NotNull(message = "The contract start date cannot be null")
  protected LocalDate startDate;

}

Architecture

The application architecture uses a "shared-library" and a "contract-service" which are two separate projects. The shared-lib contains all the models, validators and serializers which is added to the contract-service in the form of a dependency.

<dependency>
    <groupId>...</groupId>
    <artifactId>shared-lib</artifactId>
    <version>0.5.31-SNAPSHOT</version>
</dependency>

Problem

I'm working on a Spring Boot project with a model class Contract that contains a LocalDate field named startDate. This field needs to be stored as a String in MongoDB and handled as a LocalDate in Java.

There is already tons of logic in my application which uses the startDate attribute as a LocalDate, so changing the data type to String in the Contract model is not an option.

What I've tried

I've tried to implement some custom serializers that are supposed to be able to change the attribute type when being converted to JSON before it is saved to the database, but they don't seem to be working properly.

There is no error message or anything, the serializers seem to just be getting ignored totally.

JSON Serializers

public class LocalDateToStringSerializer extends StdSerializer<LocalDate> {

    private static final long serialVersionUID = 1L;

    public LocalDateToStringSerializer() {
        super(LocalDate.class);
    }

    @Override
    public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE));
    }
}
public class StringToLocalDateDeserializer extends JsonDeserializer<LocalDate> {

    @Override
    public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return LocalDate.parse(p.getValueAsString(), DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

Jackson Configuration (contract-service)

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addSerializer(LocalDate.class, new LocalDateToStringSerializer());
        module.addDeserializer(LocalDate.class, new StringToLocalDateDeserializer());
        mapper.registerModule(module);
        return mapper;
    }
}

Contract Model (using serializers)

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

@Data
@Document("contract")
public class Contract {

// ...

  /* shows the start date of the contract */
  @NotNull(message = "The contract start date cannot be null")
  @JsonSerialize(using = LocalDateToStringSerializer.class)
  @JsonDeserialize(using = StringToLocalDateDeserializer.class)
  protected LocalDate startDate;

}

Test code

This is the small test class I've set up

@Component
@Slf4j
public class ContractDatesConverter implements CommandLineRunner {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public void run(String... args) {
        Contract contract = mongoTemplate.findAll(Contract.class).get(0);

        contract.setStartDate(contract.getStartDate().plusDays(1));
        mongoTemplate.save(contract);
    }

When I run the code it changes the startDate in the database back to a LocalDate and doesn't even add a day.

Summary

I'm hopeful someone can catch my mistake or maybe suggest a more straightforward solution to the problem I'm having. Thanks!


Solution

  • One possible approach is to use a Converter for your LocalDate to String conversions. This will happen transparently and you will not have to change your Contract model.

    You need to write two converters - one for reading operation and one for writing operation.

    import org.springframework.core.convert.converter.Converter;
    import java.time.LocalDate;
    
    public class LocalDateReadConverter implements Converter<String, LocalDate> {
    
        @Override
        public LocalDate convert(String source) {
            return LocalDate.parse(source);  // default ISO_LOCAL_DATE format
        }
    }
    
    import org.springframework.core.convert.converter.Converter;
    import java.time.LocalDate;
    
    public class LocalDateWriteConverter implements Converter<LocalDate, String> {
    
        @Override
        public String convert(LocalDate source) {
            return source.toString();  // default ISO_LOCAL_DATE format
        }
    }
    

    Next, in your MongoDB configuration register these converters:

    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
    
    @Configuration
    public class MongoConfig {
    
        @Bean
        public MongoCustomConversions mongoCustomConversions() {
            List<Converter<?, ?>> converters = new ArrayList<>();
            converters.add(new LocalDateReadConverter());
            converters.add(new LocalDateWriteConverter());
            return new MongoCustomConversions(converters);
        }
    }
    

    NOTE: Jackson library isn't used by Mongo