The introduction of KotlinObjectSingletonDeserializer in 2.10.1 breaks the deserialization of Kotlin structures that could successfully be deserialized in 2.10.0.
I've forked jackson-module-kotlin and introduced three branches that demonstrate the problem and show a solution:
- I branched from tag 2.10.0 and introduced a test that passes successfully for this release - see the test here.
- I branched from tag 2.10.1 and in this branch, the same test now fails.
- I branched from master and introduced a change to
KotlinObjectSingletonDeserializer that fixes this particular issue such that the test passes again - see the change here.
To quickly try this all out just do:
$ git clone git@github.com:george-hawkins/jackson-module-kotlin.git
$ cd jackson-module-kotlin
$ git checkout 2.10.0-object-id
$ mvn -Dtest=com.fasterxml.jackson.module.kotlin.test.TestGithubX test
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
...
$ git checkout 2.10.1-object-id-bug
$ mvn -Dtest=com.fasterxml.jackson.module.kotlin.test.TestGithubX test
...
[ERROR] Errors:
[ERROR] TestGithubX.test reading involving type, id and object:63 » InvalidTypeId Miss...
...
$ git checkout master-object-id-fix
$ mvn -Dtest=com.fasterxml.jackson.module.kotlin.test.TestGithubX test
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
...
Above I check out the three branches in turn and for each ask Maven to run the test that I've added - for the 2.10.0 based branch everything works fine, for the 2.10.1 based branch things break and then in the master based branch things are fixed.
You could merge my master-object-id-fix branch to fix this particular problem. But I'm actually going to suggest reverting the addition of KotlinObjectSingletonDeserializer instead.
KotlinObjectSingletonDeserializer was introduced to try and resolve issue #244. It's certainly confusing for people if they deserialize an object and get a different instance of what was supposed to be a singleton. But Jackson has so many special cases and so much clever logic for dealing with e.g. things like cycles (@JsonIdentityInfo etc.) and with things like generics (@JsonTypeInfo etc.) that it's extremely difficult to cover all situations where Jackson deserializes, stores and reuses objects. KotlinObjectSingletonDeserializer, as it is, is just the beginning and it would require quite invasive changes elsewhere in Jackson (not just the Kotlin module) to be able to handle all cases.
Even with my change, it's still fairly trivial to construct a case where a new instance is returned by deserialization rather than the original singleton.
E.g. see this test that fails (irrespective of whether the change I made above to KotlinObjectSingletonDeserializer is applied or not). To try this just do:
$ git checkout master-object-instance-bug
$ mvn -Dtest=com.fasterxml.jackson.module.kotlin.test.TestGithubY test
...
[ERROR] test reading a list of objects with id(com.fasterxml.jackson.module.kotlin.test.TestGithubY) Time elapsed: 0.321 s <<< FAILURE!
java.lang.AssertionError: second element is not the same instance as Child.
...
I've added a detailed explanation of what's going on in this particular situation below. But in short, I think the problem is too complex to be easily addressed for all possible cases. So I think it's maybe better to just accept that unfortunately Kotlin singletons get deserialized to a different instance than to go with a solution that only works in a certain subset of simpler cases. As such I think the addition of KotlinObjectSingletonDeserializer should be reverted.
Longer explanation
The new KotlinObjectSingletonDeserializer wraps an instance of JsonDeserializer. JsonDeserializer has quite a number of potentially overrideable methods (see below) but currently KotlinObjectSingletonDeserializer only delegates the three that I've marked with an asterisk. Of the remaining 16 methods, all but one of them (marked with a minus below) is overridden in at least one subclass in the standard Jackson libraries. So it would be surprising if the fact that KotlinObjectSingletonDeserializer doesn't delegate these methods doesn't cause problems in circumstances where the given behavior is overridden in the wrapped deserializer.
* createContextual(ctxt: DeserializationContext, property: BeanProperty ): JsonDeserializer<*>
* deserialize(p: JsonParser, ctxt: DeserializationContext): T
deserialize(p: JsonParser, ctxt: DeserializationContext, intoValue: T): T
deserializeWithType(p: JsonParser, ctxt: DeserializationContext, typeDeserializer: TypeDeserializer): Any
- deserializeWithType(p: JsonParser, ctxt: DeserializationContext, typeDeserializer: TypeDeserializer, intoValue: T): Any
findBackReference(refName: String): SettableBeanProperty
getDelegatee(): JsonDeserializer<*>
getEmptyAccessPattern(): AccessPattern
getEmptyValue(ctxt: DeserializationContext): Any
getKnownPropertyNames(): Collection<Any>
getNullAccessPattern(): AccessPattern
getNullValue(ctxt: DeserializationContext): Any
getObjectIdReader(ctxt: DeserializationContext): ObjectIdReader
handledType(): Class<*>
isCachable(): Boolean
replaceDelegatee(delegatee: JsonDeserializer<*>): JsonDeserializer<*>
* resolve(ctxt: DeserializationContext)
supportsUpdate(config: DeserializationConfig): Boolean
unwrappingDeserializer(ctxt: DeserializationContext, unwrapper: NameTransformer): JsonDeserializer<T>
And even if you do delegate to the wrapped deserializer for these methods, there are still issues that KotlinObjectSingletonDeserializer can't address.
E.g. in the test I mentioned above @JsonIdentityInfo is used. When using @JsonIdentityInfo it's the delegate that stores identified instances. The following stacktrace shows where the ID based storage happens initially in this test. As you can see it happens in ObjectIdValueProperty.deserializeSetAndReturn which is called fairly deep down after KotlinObjectSingletonDeserializer has passed over control to the delegate:
* deserializeSetAndReturn:101, ObjectIdValueProperty (com.fasterxml.jackson.databind.deser.impl)
deserializeAndSet:83, ObjectIdValueProperty (com.fasterxml.jackson.databind.deser.impl)
deserializeFromObject:510, BeanDeserializer (com.fasterxml.jackson.databind.deser)
deserializeWithObjectId:1212, BeanDeserializerBase (com.fasterxml.jackson.databind.deser)
_deserializeOther:220, BeanDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:189, BeanDeserializer (com.fasterxml.jackson.databind.deser)
* deserialize:28, KotlinObjectSingletonDeserializer (com.fasterxml.jackson.module.kotlin)
_deserializeTypedForId:122, AsPropertyTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl)
deserializeTypedFromObject:89, AsPropertyTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl)
deserializeWithType:237, AbstractDeserializer (com.fasterxml.jackson.databind.deser)
_deserializeWithObjectId:384, CollectionDeserializer (com.fasterxml.jackson.databind.deser.std)
In the case of a singleton, the delegate stores its deserialized duplicate of the original object rather than the original singleton that only KotlinObjectSingletonDeserializer knows about.
When Jackson then encounters an ID for an already stored instance it will use this deserialized duplicate - it doesn't even give KotlinObjectSingletonDeserializer a chance to be involved in this process. In the above stacktrace we can see that we're deserializing a list (so CollectionDeserializer is being used) and that we're storing an element of this list that's identified with an ID. In this stacktrace below the same CollectionDeserializer has hit that ID again and, as you can see, it just looks up the ID directly, via findObjectId - it doesn't involve KotlinObjectSingletonDeserializer at all, so the stored duplicate is retrieved rather than the original singleton instance:
* findObjectId:78, DefaultDeserializationContext (com.fasterxml.jackson.databind.deser)
_deserializeFromObjectId:303, AbstractDeserializer (com.fasterxml.jackson.databind.deser)
deserializeWithType:220, AbstractDeserializer (com.fasterxml.jackson.databind.deser)
* _deserializeWithObjectId:384, CollectionDeserializer (com.fasterxml.jackson.databind.deser.std)
Hence if you look at the test you'll see the problem just with the second occurrence of the given singleton object (and you would see the same issue with any further occurrences in a longer list).
The introduction of
KotlinObjectSingletonDeserializerin 2.10.1 breaks the deserialization of Kotlin structures that could successfully be deserialized in 2.10.0.I've forked jackson-module-kotlin and introduced three branches that demonstrate the problem and show a solution:
KotlinObjectSingletonDeserializerthat fixes this particular issue such that the test passes again - see the change here.To quickly try this all out just do:
Above I check out the three branches in turn and for each ask Maven to run the test that I've added - for the 2.10.0 based branch everything works fine, for the 2.10.1 based branch things break and then in the master based branch things are fixed.
You could merge my master-object-id-fix branch to fix this particular problem. But I'm actually going to suggest reverting the addition of
KotlinObjectSingletonDeserializerinstead.KotlinObjectSingletonDeserializerwas introduced to try and resolve issue #244. It's certainly confusing for people if they deserialize anobjectand get a different instance of what was supposed to be a singleton. But Jackson has so many special cases and so much clever logic for dealing with e.g. things like cycles (@JsonIdentityInfoetc.) and with things like generics (@JsonTypeInfoetc.) that it's extremely difficult to cover all situations where Jackson deserializes, stores and reuses objects.KotlinObjectSingletonDeserializer, as it is, is just the beginning and it would require quite invasive changes elsewhere in Jackson (not just the Kotlin module) to be able to handle all cases.Even with my change, it's still fairly trivial to construct a case where a new instance is returned by deserialization rather than the original singleton.
E.g. see this test that fails (irrespective of whether the change I made above to
KotlinObjectSingletonDeserializeris applied or not). To try this just do:I've added a detailed explanation of what's going on in this particular situation below. But in short, I think the problem is too complex to be easily addressed for all possible cases. So I think it's maybe better to just accept that unfortunately Kotlin singletons get deserialized to a different instance than to go with a solution that only works in a certain subset of simpler cases. As such I think the addition of
KotlinObjectSingletonDeserializershould be reverted.Longer explanation
The new
KotlinObjectSingletonDeserializerwraps an instance ofJsonDeserializer.JsonDeserializerhas quite a number of potentially overrideable methods (see below) but currentlyKotlinObjectSingletonDeserializeronly delegates the three that I've marked with an asterisk. Of the remaining 16 methods, all but one of them (marked with a minus below) is overridden in at least one subclass in the standard Jackson libraries. So it would be surprising if the fact thatKotlinObjectSingletonDeserializerdoesn't delegate these methods doesn't cause problems in circumstances where the given behavior is overridden in the wrapped deserializer.And even if you do delegate to the wrapped deserializer for these methods, there are still issues that
KotlinObjectSingletonDeserializercan't address.E.g. in the test I mentioned above
@JsonIdentityInfois used. When using@JsonIdentityInfoit's the delegate that stores identified instances. The following stacktrace shows where the ID based storage happens initially in this test. As you can see it happens inObjectIdValueProperty.deserializeSetAndReturnwhich is called fairly deep down afterKotlinObjectSingletonDeserializerhas passed over control to the delegate:In the case of a singleton, the delegate stores its deserialized duplicate of the original object rather than the original singleton that only
KotlinObjectSingletonDeserializerknows about.When Jackson then encounters an ID for an already stored instance it will use this deserialized duplicate - it doesn't even give
KotlinObjectSingletonDeserializera chance to be involved in this process. In the above stacktrace we can see that we're deserializing a list (soCollectionDeserializeris being used) and that we're storing an element of this list that's identified with an ID. In this stacktrace below the sameCollectionDeserializerhas hit that ID again and, as you can see, it just looks up the ID directly, viafindObjectId- it doesn't involveKotlinObjectSingletonDeserializerat all, so the stored duplicate is retrieved rather than the original singleton instance:Hence if you look at the test you'll see the problem just with the second occurrence of the given singleton object (and you would see the same issue with any further occurrences in a longer list).