Description
I want to use Map.of()
in my execution context (primarily for tests). But this gives an error:
val serializer = new Jackson2ExecutionContextStringSerializer();
val out = new ByteArrayOutputStream();
serializer.serialize(Map.of("f", Map.of("f1", "v1")), out);
val json = out.toString();
System.out.println(json);
serializer.deserialize(new ByteArrayInputStream(json.getBytes()));
Output:
{"f":{"@class":"java.util.ImmutableCollections$Map1","f1":"v1"}}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [map type; class java.util.HashMap, [simple type, class java.lang.String] -> [simple type, class java.lang.Object]]: missing type id property '@class'
The problem is that the typing inside Jackson2ExecutionContextStringSerializer
is configured to ObjectMapper.DefaultTyping.NON_FINAL
which skips the addition of @class
property to the root object for some reason, even though ImmutableCollections$Map1
is final
. So, as you can see, only the inner map has it. But if we try to serialize new HashMap<>(Map.of("f", Map.of("f1", "v1")))
then it does include it for the root.
One solution to it is to use ObjectMapper.DefaultTyping.EVERYTHING
or enable MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL
so that the ObjectMapper
doesn't need to check against @class
property if it's given the type at compile time.
It knows that it's supposed to be a HashMap
from Jackson2ExecutionContextStringSerializer#deserialize
:
public Map<String, Object> deserialize(InputStream in) throws IOException {
TypeReference<HashMap<String,Object>> typeRef = new TypeReference<HashMap<String,Object>>() {};
return objectMapper.readValue(in, typeRef);
}
So, with enabling the feature, another error comes out:
val serializer = new Jackson2ExecutionContextStringSerializer();
((ObjectMapper) FieldUtils.readField(serializer, "objectMapper", true))
.enable(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL);
val out = new ByteArrayOutputStream();
serializer.serialize(Map.of("f", Map.of("f1", "v1")), out);
val json = out.toString();
System.out.println(json);
serializer.deserialize(new ByteArrayInputStream(json.getBytes()));
The output:
{"f":{"@class":"java.util.ImmutableCollections$Map1","f1":"v1"}}
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: The class with java.util.ImmutableCollections$Map1 and name of java.util.ImmutableCollections$Map1 is not trusted. If you believe this class is safe to deserialize, you can add it to the base set of trusted classes at construction time or provide an explicit mapping using Jackson annotations or a custom ObjectMapper. If the serialization is only done by a trusted source, you can also enable default typing. (through reference chain: java.util.HashMap["f"])
I think all java.util.ImmutableCollections$*
should be enabled by default.
With adding those, another error comes out:
val serializer = new Jackson2ExecutionContextStringSerializer(
"java.util.ImmutableCollections$ListN",
"java.util.ImmutableCollections$List12",
"java.util.ImmutableCollections$SubList",
"java.util.ImmutableCollections$Set12",
"java.util.ImmutableCollections$SetN",
"java.util.ImmutableCollections$Map1",
"java.util.ImmutableCollections$MapN"
);
((ObjectMapper) FieldUtils.readField(serializer, "objectMapper", true))
.enable(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL);
val out = new ByteArrayOutputStream();
serializer.serialize(Map.of("f", Map.of("f1", "v1")), out);
val json = out.toString();
System.out.println(json);
val res = serializer.deserialize(new ByteArrayInputStream(json.getBytes()));
System.out.println(res);
The output:
{"f":{"@class":"java.util.ImmutableCollections$Map1","f1":"v1"}}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.util.ImmutableCollections$Map1` (no Creators, like default constructor, exist): no default constructor found
That's a Jackson issue FasterXML/jackson-databind#2900 that's already been fixed in 2.13.0
. So, after updating the Jackson library to 2.13.0
the last code snippet gives the correct output:
{"f":{"@class":"java.util.ImmutableCollections$Map1","f1":"v1"}}
{f={f1=v1}}
Environment
Spring Batch 4.3.3
Spring Boot 2.5.0
JDK Amazon Corretto 11.0.13