jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

`InvalidDefinitionException: Java 8 date/time type java.time.LocalDateTime...` when calling `mapper.createObjectNode().putPOJO`

Open jebbench opened this issue 2 years ago • 7 comments

Hello,

When running the code below I get the exception:

Exception in thread "main" java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
	at com.fasterxml.jackson.databind.node.InternalNodeMapper.nodeToPrettyString(InternalNodeMapper.java:40)
	at com.fasterxml.jackson.databind.node.BaseJsonNode.toPrettyString(BaseJsonNode.java:141)
	at Main.main(Main.java:16)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
	at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1300)
	at com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer.serialize(UnsupportedTypeSerializer.java:35)
	at com.fasterxml.jackson.databind.SerializerProvider.defaultSerializeValue(SerializerProvider.java:1142)
	at com.fasterxml.jackson.databind.node.POJONode.serialize(POJONode.java:115)
	at com.fasterxml.jackson.databind.node.ObjectNode.serialize(ObjectNode.java:328)
	at com.fasterxml.jackson.databind.ser.std.SerializableSerializer.serialize(SerializableSerializer.java:39)
	at com.fasterxml.jackson.databind.ser.std.SerializableSerializer.serialize(SerializableSerializer.java:20)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
	at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1518)
	at com.fasterxml.jackson.databind.ObjectWriter._writeValueAndClose(ObjectWriter.java:1219)
	at com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(ObjectWriter.java:1086)
	at com.fasterxml.jackson.databind.node.InternalNodeMapper.nodeToPrettyString(InternalNodeMapper.java:38)
	... 2 more

2.11.4 works as expected. 2.13.0-rc2 fails with the same error.

Main.java

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import java.time.LocalDateTime;

public class Main {

    public static void main(String[] args) {
        ObjectMapper mapper = JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .build();

        ObjectNode node = mapper.createObjectNode().putPOJO("test", LocalDateTime.now());
        System.out.println(node.toPrettyString());
    }

}

build.gradle

plugins {
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation platform("com.fasterxml.jackson:jackson-bom:2.12.5")
    implementation "com.fasterxml.jackson.core:jackson-databind"
    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
}

I've tried using AdoptOpenJDK (OpenJ9) 11.0.11 and AdoptOpenJDK (Hotspot) 14.0.2 and I get the same result on both.

jebbench avatar Aug 27 '21 16:08 jebbench

This is intentional: as per #2683, Java 8 date/time types cannot be serialized (as POJOs) since those cannot be read back (deserialized). The solution is to register module jackson-datatype-jsr353 (see https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr353 or https://github.com/FasterXML/jackson-modules-java8/), which will add expected serializer.

cowtowncoder avatar Aug 27 '21 21:08 cowtowncoder

I think I am adding the module:

ObjectMapper mapper = JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .build();

It doesn't actually work in 2.11, it just serializes the date without using the module.

I looks like calling any accessor methods on ObjectNode ignores any modules loaded in the Object Mapper which used to create it.

My use case is in a unit test checking that the ObjectNode is built correctly - in running code it's not an issue as I always do objectMapper.writeValue but in my tests I just want to check the updatedAt key is correct (node.get("updatedAt")).

jebbench avatar Aug 31 '21 08:08 jebbench

Looking at this, the actual problem is the ObjectNode.toString() -- it cannot serialize POJOs. Proper way here would be to use ObjectMapper:

mapper.writeValueAsString(node);

The problem with toString() is that ObjectNode has no connection to real ObjectMapper, and that is why there's no way to add module. Using a full ObjectMapper will resolve this issue.

I'll have to think about this for a bit; behavior is not optimal but I am not sure what could be done to improve it.

cowtowncoder avatar Sep 01 '21 03:09 cowtowncoder

What would be the best way to retrieve a value added using putPOJO?

I have a unit test where I want to make sure that the value added at a particular key is the expected value (I can cope with getting back the POJO or the equivalent JSON value).

LocalDateTime now = LocalDateTime.now();
ObjectNode node = mapper.createObjectNode().putPOJO("test", now);

assertThat(node.get("test")).isEqualTo(now));
// or
assertThat(node.get("test")).isEqualTo(now.toString()));

Would it be possible to add a getPOJO method to allow access to the original POJO?

jebbench avatar Sep 01 '21 11:09 jebbench

POJONode has method getPojo() -- but you need to cast the type from JsonNode to call it.

cowtowncoder avatar Sep 01 '21 15:09 cowtowncoder

If the Temporal is created as POJONode, this exception will be thrown because of the BaseJsonNode::toString()

 @Override
   public String toString() {
       return InternalNodeMapper.nodeToString(this);
   }

liang-shen avatar Aug 30 '22 09:08 liang-shen

At this point I can reproduce the issue but don't really know if anything may be done -- my suggestion is that when using "POJO" nodes, use mapper.writeValueAsString() and not rely on JsonNode.toString() as that can never really have support for external types.

The problem then being that of error handling as there seem to be only bad choices:

  1. Throw an unchecked exception (which is surprising from toString())
  2. Produce invalid JSON (like truncate, or add exception info)
  3. Produce valid JSON but one that is different from what one would get from proper serialization (like JSON String with error message)

cowtowncoder avatar Aug 30 '22 20:08 cowtowncoder