Deserializing date/time in Spring Data MongoDB

39 views Asked by At

I'm using Spring Data MongoDB to manage the communication with MongoDB instance. I have a simple document class containing id and Instant fields:

@Document
data class Test(
    @MongoId(FieldType.STRING)
    override var id: UUID,
    override val updatedAt: Instant,
)

An instance of this document is serialized into Mongo as (or actually showed in the Mongo shell as)

  {
    _id: '7ea1a5ff-0c3c-451f-9ebd-99def53541f5',
    updatedAt: ISODate('2023-10-09T11:43:51.152Z'),
  }

When used in the Spring app the deserialization works without issues. However, for testing purposes, I would also like to read documents from stored JSON files. The JSON serialization looks like this:

  {
    "_id": "c11f01c3-24a8-4321-a0ce-beb041207e5b",
    "updatedAt": {
      "$date": "2024-03-04T13:53:01.658Z"
    },
  }

I'm using the Spring-configured ObjectMapper instance for deserializing this (I assume the same instance that is used in the app), so I would expect that everything works as expected. However, apparently the ObjectMapper isn't aware of the MongoDB-specifics so the deserialization fails with

    Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.time.Instant` from Object value (token `JsonToken.START_OBJECT`)

How to configure the ObjectMapper to properly handle MongoDB dates/times?

Note: The exact way how I'm reading the test data is this:

    @Bean
    fun repositoryPopulator(objectMapper: ObjectMapper) =
        Jackson2RepositoryPopulatorFactoryBean().apply {
            setMapper(objectMapper)
            setResources(paths)
        }

This is in a config class imported into @SpringBootTest so that's why I assume that the ObjectMapper instance should be the same as in the main app. But apparently it isn't.

1

There are 1 answers

4
Michael Gantman On

So, the solution is that you have to add the following annotation over your Test class property updatedAt

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX", timezone = "UTC")

After that the following code will work:

    private static void jsonParserInstantTest() {
        Instant time = Instant.now();
        InstantHolder instantHolder = new InstantHolder();
        instantHolder.setCurrentTime(time);
        instantHolder.setId("test");
        try {

            ObjectMapper om = new ObjectMapper().registerModule(new JavaTimeModule());
            String serialized = om.writeValueAsString(instantHolder);

            System.out.println(serialized);

            InstantHolder restored = om.readValue(serialized, InstantHolder.class);

            System.out.println(restored);
        } catch (IOException ioe) {
            ...
        }
    }

The output of this will be:

{"id":"test","currentTime":"2024-03-05T18:23:47.162Z"}
InstantHolder{id='test', currentTime=2024-03-05T18:23:47.162Z}

The InstantHolder class that I used is a very simple one:

    package com.mgnt.stam;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    import java.time.Instant;
    
    public class InstantHolder {
        private String id;
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX", timezone = "UTC")
        private Instant currentTime;

// Getter and setter methods are trivial and are omitted here to save space    

        @Override
        public String toString() {
            return "InstantHolder{" +
                    "id='" + id + '\'' +
                    ", currentTime=" + currentTime +
                    '}';
        }
    }

So this provides a solution. But I can offer you a slight optimization where you don't have to instantiate and configure ObjectMapper class. Consider the client code like this:

private static void jsonParserInstantTest() {
    Instant time = Instant.now();
    InstantHolder instantHolder = new InstantHolder();
    instantHolder.setCurrentTime(time);
    instantHolder.setId("test");
    try {

        String serialized = JsonUtils.writeObjectToJsonString(instantHolder);

        System.out.println(serialized);

        InstantHolder restored = JsonUtils.readObjectFromJsonString(serialized, InstantHolder.class);

        System.out.println(restored);
    } catch (IOException ioe) {
        ...
    }
}

This will work exactly the same. However, you can see here the use of static methods of class JsonUtils. This class is a thin wrapper over ObjectMapper class and it comes as part of MgntUtils Open source library (written and maintained by me). Here is Javadoc for JsonUtils class. The library can be obtained as a Maven artifact from Maven central or from Github as a jar file as well as Javadoc and source code