-3

I am facing an issue with configuring the Jackson ObjectMapper to deserialize java.sql.Timestamp. In particular, the issue is that deserialization with a mapper that was initialized via ObjectMapper mapper = new ObjectMapper() works, while using a globally registered one that is dependency injected by spring does not. I do not know how the global one is declared, which makes it hard to debug which setting exactly makes it not work.

So my question is basically what ObjectMapper configurations/modules are exactly required to deserialize java.sql.Timestamp? Note that I do not want to define any (field specific format) annotations on MyClass!

Class to deserialize

import java.sql.Timestamp;

public class MyClass {
  private Timestamp startTime;
}

Json

{
  "startTime":"2025-07-08T09:29:54.000+0000"
}

Test case

@SpringBootTest
public class DeserializationTest {

    @Autowired
    ObjectMapper injectedMapper;

    @Test
    public void foo() throws JsonMappingException, JsonProcessingException {
        ObjectMapper localMapper = new ObjectMapper();
        String json = "{\"startTime\":\"2025-07-08T09:29:54.000+0000\"}";

        localMapper.readValue(json, MyClass.class);    // this works
        injectedMapper.readValue(json, MyClass.class); // this fails with InvalidFormat

        assertTrue(true);
    }
}

The injectedMapper.readValue(...) invocation fails with InvalidFormat Cannot deserialize value of type 'java.lang.Long' from String "2025-07-08T09:29:54.000+0000": not a valid 'java.lang.Long' value

I know there are already a couple of questions in this direction, but the answers typically recommend annotations on the class that should be deserialized or have custom timestamp formats. I am looking for a configuration on the ObjectMapper definition instead.

Edit: I also don't want to register custom deserializers, since the fact that new ObjectMapper().readValue(...) successfully deserializes the json tells me that there must be out-of-the-box configuration that makes this work without anything custom

Edit2: Comparison of ObjectMapper settings: new ObjectMapper()

Modules:
    -none-

Serialization Features:
    WRAP_ROOT_VALUE -> false
    INDENT_OUTPUT -> false
    FAIL_ON_EMPTY_BEANS -> true (enabled by default)
    FAIL_ON_SELF_REFERENCES -> true (enabled by default)
    WRAP_EXCEPTIONS -> true (enabled by default)
    FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS -> true (enabled by default)
    WRITE_SELF_REFERENCES_AS_NULL -> false
    CLOSE_CLOSEABLE -> false
    FLUSH_AFTER_WRITE_VALUE -> true (enabled by default)
    WRITE_DATES_AS_TIMESTAMPS -> true (enabled by default)
    WRITE_DATE_KEYS_AS_TIMESTAMPS -> false
    WRITE_DATES_WITH_ZONE_ID -> false
    WRITE_DATES_WITH_CONTEXT_TIME_ZONE -> true (enabled by default)
    WRITE_DURATIONS_AS_TIMESTAMPS -> true (enabled by default)
    WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS -> false
    WRITE_ENUMS_USING_TO_STRING -> false
    WRITE_ENUMS_USING_INDEX -> false
    WRITE_ENUM_KEYS_USING_INDEX -> false
    WRITE_NULL_MAP_VALUES -> true (enabled by default)
    WRITE_EMPTY_JSON_ARRAYS -> true (enabled by default)
    WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED -> false
    WRITE_BIGDECIMAL_AS_PLAIN -> false
    WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS -> true (enabled by default)
    ORDER_MAP_ENTRIES_BY_KEYS -> false
    EAGER_SERIALIZER_FETCH -> true (enabled by default)
    USE_EQUALITY_FOR_OBJECT_ID -> false

Deserialization Features:
    USE_BIG_DECIMAL_FOR_FLOATS -> false
    USE_BIG_INTEGER_FOR_INTS -> false
    USE_LONG_FOR_INTS -> false
    USE_JAVA_ARRAY_FOR_JSON_ARRAY -> false
    FAIL_ON_UNKNOWN_PROPERTIES -> true (enabled by default)
    FAIL_ON_NULL_FOR_PRIMITIVES -> false
    FAIL_ON_NUMBERS_FOR_ENUMS -> false
    FAIL_ON_INVALID_SUBTYPE -> true (enabled by default)
    FAIL_ON_READING_DUP_TREE_KEY -> false
    FAIL_ON_IGNORED_PROPERTIES -> false
    FAIL_ON_UNRESOLVED_OBJECT_IDS -> true (enabled by default)
    FAIL_ON_MISSING_CREATOR_PROPERTIES -> false
    FAIL_ON_NULL_CREATOR_PROPERTIES -> false
    FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY -> true (enabled by default)
    FAIL_ON_TRAILING_TOKENS -> false
    WRAP_EXCEPTIONS -> true (enabled by default)
    ACCEPT_SINGLE_VALUE_AS_ARRAY -> false
    UNWRAP_SINGLE_VALUE_ARRAYS -> false
    UNWRAP_ROOT_VALUE -> false
    ACCEPT_EMPTY_STRING_AS_NULL_OBJECT -> false
    ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT -> false
    ACCEPT_FLOAT_AS_INT -> true (enabled by default)
    READ_ENUMS_USING_TO_STRING -> false
    READ_UNKNOWN_ENUM_VALUES_AS_NULL -> false
    READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE -> false
    READ_DATE_TIMESTAMPS_AS_NANOSECONDS -> true (enabled by default)
    ADJUST_DATES_TO_CONTEXT_TIME_ZONE -> true (enabled by default)
    EAGER_DESERIALIZER_FETCH -> true (enabled by default)

Autowired one:

Modules:
  com.fasterxml.jackson.datatype.jdk8.Jdk8Module
  jackson-datatype-jsr310
  jackson-module-kotlin
  jackson-module-parameter-names
  org.springframework.boot.jackson.JsonComponentModule
  Spring Data Geo Mixins

Serialization Features:
    WRAP_ROOT_VALUE -> false
    INDENT_OUTPUT -> false
    FAIL_ON_EMPTY_BEANS -> true (enabled by default)
    FAIL_ON_SELF_REFERENCES -> true (enabled by default)
    WRAP_EXCEPTIONS -> true (enabled by default)
    FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS -> true (enabled by default)
    WRITE_SELF_REFERENCES_AS_NULL -> false
    CLOSE_CLOSEABLE -> false
    FLUSH_AFTER_WRITE_VALUE -> true (enabled by default)
    WRITE_DATES_AS_TIMESTAMPS -> false (enabled by default)
    WRITE_DATE_KEYS_AS_TIMESTAMPS -> false
    WRITE_DATES_WITH_ZONE_ID -> false
    WRITE_DATES_WITH_CONTEXT_TIME_ZONE -> true (enabled by default)
    WRITE_DURATIONS_AS_TIMESTAMPS -> false (enabled by default)
    WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS -> false
    WRITE_ENUMS_USING_TO_STRING -> false
    WRITE_ENUMS_USING_INDEX -> false
    WRITE_ENUM_KEYS_USING_INDEX -> false
    WRITE_NULL_MAP_VALUES -> true (enabled by default)
    WRITE_EMPTY_JSON_ARRAYS -> true (enabled by default)
    WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED -> false
    WRITE_BIGDECIMAL_AS_PLAIN -> false
    WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS -> true (enabled by default)
    ORDER_MAP_ENTRIES_BY_KEYS -> false
    EAGER_SERIALIZER_FETCH -> true (enabled by default)
    USE_EQUALITY_FOR_OBJECT_ID -> false

Deserialization Features:
    USE_BIG_DECIMAL_FOR_FLOATS -> false
    USE_BIG_INTEGER_FOR_INTS -> false
    USE_LONG_FOR_INTS -> false
    USE_JAVA_ARRAY_FOR_JSON_ARRAY -> false
    FAIL_ON_UNKNOWN_PROPERTIES -> false (enabled by default)
    FAIL_ON_NULL_FOR_PRIMITIVES -> false
    FAIL_ON_NUMBERS_FOR_ENUMS -> false
    FAIL_ON_INVALID_SUBTYPE -> true (enabled by default)
    FAIL_ON_READING_DUP_TREE_KEY -> false
    FAIL_ON_IGNORED_PROPERTIES -> false
    FAIL_ON_UNRESOLVED_OBJECT_IDS -> true (enabled by default)
    FAIL_ON_MISSING_CREATOR_PROPERTIES -> false
    FAIL_ON_NULL_CREATOR_PROPERTIES -> false
    FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY -> true (enabled by default)
    FAIL_ON_TRAILING_TOKENS -> false
    WRAP_EXCEPTIONS -> true (enabled by default)
    ACCEPT_SINGLE_VALUE_AS_ARRAY -> false
    UNWRAP_SINGLE_VALUE_ARRAYS -> false
    UNWRAP_ROOT_VALUE -> false
    ACCEPT_EMPTY_STRING_AS_NULL_OBJECT -> false
    ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT -> false
    ACCEPT_FLOAT_AS_INT -> true (enabled by default)
    READ_ENUMS_USING_TO_STRING -> false
    READ_UNKNOWN_ENUM_VALUES_AS_NULL -> false
    READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE -> false
    READ_DATE_TIMESTAMPS_AS_NANOSECONDS -> true (enabled by default)
    ADJUST_DATES_TO_CONTEXT_TIME_ZONE -> true (enabled by default)
    EAGER_DESERIALIZER_FETCH -> true (enabled by default)

Just the properties that differ:

Modules:
  -none-
-------
Modules:
  com.fasterxml.jackson.datatype.jdk8.Jdk8Module
  jackson-datatype-jsr310
  jackson-module-kotlin
  jackson-module-parameter-names
  org.springframework.boot.jackson.JsonComponentModule
  Spring Data Geo Mixins

Serialization Features:
  WRITE_DATES_AS_TIMESTAMPS -> true (enabled by default)
  WRITE_DURATIONS_AS_TIMESTAMPS -> true (enabled by default)
-------
  WRITE_DATES_AS_TIMESTAMPS -> false (enabled by default)
  WRITE_DURATIONS_AS_TIMESTAMPS -> false (enabled by default)

Deserialization Features:
  FAIL_ON_UNKNOWN_PROPERTIES -> true (enabled by default)
-------
  FAIL_ON_UNKNOWN_PROPERTIES -> false (enabled by default)

I would have expected other deserialization features that would be different, but it's only FAIL_ON_UNKNOWN_PROPERTIES. I also tried registering the modules on the local mapper, expecting that this may break the deserialization for it then, but it didn't

12
  • Does this help link ? Commented Jul 8 at 7:52
  • 5
    Create a custom deserializer for timestamp. Or even better, stop using Timestamp and use the proper ones from java.time instead. Timestamp is a very badly designed class (well anything that extends java.util.Date for that matter) and should be avoided. Commented Jul 8 at 7:58
  • Thanks for the answers so far. I do not want to have a custom deserializer. The fact that new ObjectMapper().readValue(...) is able to successfully deserialize to Timestamp tells me that it is possible to configure the ObjectMapper without any custom annotations or serializers. I'll edit my question again Commented Jul 8 at 8:10
  • @pebbleunit that is actually quite hard to figure out, since it is provided by the frameworks autoconfiguration. That's why I am hoping that someone can tell me the settings that influence this deserialization behaviour for timestamps. I already did a lot of trial and error but I could not find the right settings for it yet... Commented Jul 8 at 8:21
  • 1
    Can you dump the configuration of your injected ObjectMapper, to see how Spring configures it? Commented Jul 8 at 9:11

1 Answer 1

-1

Since the configuration of your mappers doesn't explain the behavior, I assume that there is a custom deserializer at work. Unfortunately, I don't know any way to get the information out of Jackson, which deserializers it has registered.

But you can use the "classgraph" library to list all candidates on your classpath.

You are looking for a class that serializes java.sql.Timestamp into a Long.

For example using Maven:

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph</artifactId>
    <version>4.8.180</version>
</dependency>

And then, somewhere in your code:

try (ScanResult scanResult = new ClassGraph().enableClassInfo().scan()) {
    scanResult.getSubclasses(JsonDeserializer.class.getName())
            .forEach(cls -> {
                if (cls.getName().toLowerCase().contains("timestamp")) {
                    System.out.println("Timestamp-related deserializer: " + cls.getName());
                }
             });
}

If this outputs anything else other than Jacksons default TimestampDeserializer ...

Timestamp-related deserializer: com.fasterxml.jackson.databind.deser.std.DateDeserializers$TimestampDeserializer

... then this is likely the culprit.

Be aware of the filter .contains("timestamp"), you should probably look out for classes that are not in the com.fasterxml.jackson namespace.

Sign up to request clarification or add additional context in comments.

10 Comments

The configuration does explain it. One has several modules installed leading to processing java.util.Date which is what java.sql.Timestamp extends. A plain ObjectMapper doesn't have this and, AFAIK, will try to find a valueOf method with the input type (String in this case) to construct the object.
I think it is possible to have Jackson load a deserializer, which is not provided by a module, and would not show up in this output. Are you aware of the specific one we are looking for?
@M.Deinum I tried registering all the modules listed on the local mapper and the deserialization unfortunately still worked with it. I also couldn't find an explicit valueOf method on the real class
@thomashirsch I tried the classpath scanner and got the same output as you
docs.oracle.com/javase/8/docs/api/java/sql/… (it clearly is there for quite some time). As stated the fact that the modules are registered is the fact that it uses certain default patterns, if you don't it wiill just call the valueOf (which is clearly on the Timestamp class). You would need to register your format with Jackson so it will be parsed (and as stated before I would strongly suggest to not let your domain objects to rely on the java.sql.Timestamp).
And why does the local mapper still work when registering all the same modules that are registered on the global one?!
There exist various ways to tell Jackson to use a certain class for deserialization, that would not require registering a module. For example, you could put an annotation directly on the target class: @JsonDeserialize(using = ItemDeserializer.class). This is the reason I asked you to inspect the classpath, including libraries, for all possible candidates.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.