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

`ObjectMapper.readTree(…)` causes `InvalidTypeIdException`

Open mp911de opened this issue 2 years ago • 3 comments

Describe the bug Not sure this is a reincarnation of #793. Attempting to read a JSON into JsonNode fails when the ObjectMapper is initialized with default typing.

Version information 2.13.2

To Reproduce

final class FinalObject {
	public Long longValue;
	SimpleObject simpleObject;

	public Long getLongValue() {
		return longValue;
	}

	public void setLongValue(Long longValue) {
		this.longValue = longValue;
	}

	public SimpleObject getSimpleObject() {
		return simpleObject;
	}

	public void setSimpleObject(SimpleObject simpleObject) {
		this.simpleObject = simpleObject;
	}
}

FinalObject source = new FinalObject();
source.longValue = 1L;
source.simpleObject = new SimpleObject(2L);

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING, As.PROPERTY);

byte[] bytes = mapper.writeValueAsBytes(source);

mapper.readTree(bytes); // <--- InvalidTypeIdException

Expected behavior readTree(…) should allow reading JSON into a tree to bypass JSON to object mapping.

Exception

om.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$FinalObject' as a subtype of `com.fasterxml.jackson.databind.JsonNode`: Not a subtype
 at [Source: (byte[])"{"@class":"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$FinalObject","longValue":["java.lang.Long",1],"simpleObject":{"@class":"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$SimpleObject","longValue":["java.lang.Long",2]}}"; line: 1, column: 11]

	at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:43)
	at com.fasterxml.jackson.databind.DeserializationContext.invalidTypeIdException(DeserializationContext.java:2073)
	at com.fasterxml.jackson.databind.DatabindContext._throwNotASubtype(DatabindContext.java:276)
	at com.fasterxml.jackson.databind.DatabindContext.resolveAndValidateSubType(DatabindContext.java:237)
	at com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver._typeFromId(ClassNameIdResolver.java:72)
	at com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver.typeFromId(ClassNameIdResolver.java:66)
	at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:159)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:125)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:110)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromAny(AsPropertyTypeDeserializer.java:213)
	at com.fasterxml.jackson.databind.deser.std.BaseNodeDeserializer.deserializeWithType(JsonNodeDeserializer.java:220)
	at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserializeWithType(JsonNodeDeserializer.java:20)
	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
	at com.fasterxml.jackson.databind.ObjectMapper._readTreeAndClose(ObjectMapper.java:4716)
	at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:3090)

mp911de avatar Jun 03 '22 08:06 mp911de

As a general rule, I would advise strongly against ever using DefaultTyping.EVERYTHING. If that is used, attempt is made to add type for all kinds of things for which polymorphic type id is not necessary or useful. Including but not limited to JsonNode.

Now... whether usage above is valid or invalid I can't say off-hand. As the general rule, if polymorphic handling (esp. default typing is enabled), conversions are off the table: what is written must really be written with same nominal base type. And since readTree() is basically a convenience alias for readValue(source, JsonNode.class), it would be invalid usage. But then again, conceptually, Tree Model (JsonNode) should be the "raw" representation of content (in ideal world). So it should (ideally) be possible to use such read which would effectively disable use of Default Typing.

For time being I would suggest having different ObjectMapper for reading content in such cases.\

I am not sure whether handling like above can be supported in near future.

Also: as to EVERYTHING, it really really should not be used. I wish I had not agreed to add that setting. Instead of this, better approach tends to be to enable polymorphic handling via wrapper value:

public class MyTypedWrapper {
   @JsonTypeInfo(....)
   public Object value;
}

in which case whatever value is used will use polymorphic typing. It does require one more level of wrapping (extra { value: ... } in JSON) but works generally better than Default Typing.

cowtowncoder avatar Jun 06 '22 20:06 cowtowncoder

Thanks a lot for the insights. In our case, we're providing a generic serialization utility within a framework to handle all Java classes for an application, including Kotlin-compiled classes.

Trying to avoid ˋEVERYTHINGˋ ended up in a lot of custom code that didn't look to us as if it was intended that way.

We can give it another spin. For the time being, having another ObjectMapper instance is what we went for.

I think we can close this ticket as the reasoning why things work like they do makes sense.

mp911de avatar Jun 08 '22 05:06 mp911de

@mp911de Instead of EVERYTHING, it would probably make sense to have a custom handler to determine the rules -- out-of-the-box options may not be ideal for your use case. I forget exactly which handler class to reimplement, but I hope looking at config methods for ObjectMapper (to activate default typing) help. And then checking default choice implementations (rules are pretty trivial) hopefully helps how to change.

Also: you probably tried this already but NON_FINAL was intended to be one that should work for most case -- there should rarely be need to force type id if base type is final.

cowtowncoder avatar Jun 08 '22 16:06 cowtowncoder