Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite Bean Property Introspection logic in Jackson 2.x #4515

Closed
cowtowncoder opened this issue May 5, 2024 · 27 comments
Closed

Rewrite Bean Property Introspection logic in Jackson 2.x #4515

cowtowncoder opened this issue May 5, 2024 · 27 comments
Labels
Milestone

Comments

@cowtowncoder
Copy link
Member

Describe your Issue

We need to rewrite the Bean Property introspection (see #3719 for background); and while this is ambitious undertaking, I think it should be done in Jackson 2.x timeframe, not 3.x. That way code remains potentially mergeable from 2.x to 3.x.

The main drivers for rewrite are to solve problems that are difficult if not impossible to solve with the current mechanism:

  1. Merging of annotations between Field/Method accessors and Creator (constructor/factory method) arguments
  2. Handling of Record types and in particular alternate (non-canonical) constructors
  3. Support for canonical constructors for non-Java JVM languages (Kotlin, Scala)

This issue is an umbrella ticket as refactoring will likely be spread across multiple PRs; they can all refer to this issue,

@cowtowncoder cowtowncoder added to-evaluate Issue that has been received but not yet evaluated 2.18 and removed to-evaluate Issue that has been received but not yet evaluated labels May 5, 2024
@cowtowncoder
Copy link
Member Author

Started reading the code and clearly more of code wrt Creator detection needs to move from BeanDeserializerFactory/BasicDeserializerFactory (later part of processing) into POJOPropertiesCollector (earlier part of processing).

But not sure whether to try to move abstractions from Factory part, or rewrite; and whether to try move things incrementally or not.

Started removing deprecated methods from code paths, to help isolate actual in-use code.

@JooHyukKim
Copy link
Member

But not sure whether to try to move abstractions from Factory part, or rewrite; and whether to try move things incrementally or not.

If we agree that we have enough test coverage, I guess.... rewrite, move big-step-incrementally?

@cowtowncoder
Copy link
Member Author

cowtowncoder commented May 14, 2024

@JooHyukKim Yes, I think test coverage is sufficient to allow refactoring. I am struggling at finding the steps tho. Especially in a way that would allow merging 2.18 -> master.
So it's not yet about mechanics but even conceptually which things to move or rewrite/re-implement.

And one problem part is that the latest BeanDeserializerFactory code for processing Creators relies on [Basic]BeanDescription, which would not be available during POJOPropertiesCollector (since BeanDescription is mostly used as wrapper for actual POJOPropertiesCollector.

@cowtowncoder
Copy link
Member Author

Ok one big challenge that is coming up now, looking through functionality in BasicDeserializerFactory is the coupling to deserialization side in general (DeserializationConfig and DeserializationContext), and also dependency on context (DeserializationContext). This because POJOPropertiesCollector only operates on generic MapperConfig (base config of DeserializationConfig and SerializationConfig), and does not need or use context.
Dependencies are not trivial to remove; but they are also not exactly essential. But just means it is not easy to start by "lift-and-shift"ing (that is, by basically moving code from one place to the other), some rewriting required.

In the meantime I am making small incremental changes, including removal of deprecated code in affected areas.

@JooHyukKim
Copy link
Member

Right. Also from my experience, many times it was necessary to both modify existing and add additional "internal" structure to make the code base in better shape. More of "tidy up just before making changes".

Just out of curiosity, are we going to introduce POJOPropertiesCollector, RecordPropertiesCollector and such for new version? This question is just from my imagination 😆. I am asking this because, though not necessary, if there's any plan in written format that I can refer to, so I can help.

@cowtowncoder
Copy link
Member Author

Just out of curiosity, are we going to introduce POJOPropertiesCollector, RecordPropertiesCollector and such for new version? 

Not planned yet. I am thinking it should be possible to make things work without fully separate implementation. But of course if it becomes necessary that could be done.

At this first I would really want to make existing logic work same as now, but get Creator properties discovered around time others are... so move it out of BasicDeserializerFactory into either POJOPropertiesCollector or where BasicBeanDescription is resolved.

@cowtowncoder
Copy link
Member Author

cowtowncoder commented May 15, 2024

Hmmmh. Despite incremental refactoring, I don't think functional from BasicDeserializerFactory can moved as-is into POJOPropertiesCollector -- it does "too much", including instantiation of deserializers and so on.
So what we need is a subset. Need to re-think things a bit.

...

Ok. So, in POJOPropertiesCollector I think handling needs to incorporate selection of actual Creator method and NOT just individual properties. And that information will then be used by BasicDeserializerFactory.

@cowtowncoder
Copy link
Member Author

Good progress:

  1. All JDK-8 tests pass with re-factored POJOPropertiesCollector
  2. Only 4 Record tests fail
  3. Still have duplication with BasicDeserializerFactory: will leave resolving that to second PR

Hoping to solve remaining Record tests, to get first PR merged into 2.18, then master.

@cowtowncoder
Copy link
Member Author

Forgot to add an update: #4532 was merged 2 days ago, and it handles front-end half of changes:

  • POJOPropertiesCollector fully resolves explicit Properties-based creators, linking properties
  • Code in BasicDeserializerFactory still handles rest of introspection, including linking from (1) to Creators. This is to be removed as the next step.

I also started to look into the second half (reducing/removing code from BasicDeserializerFactory); it is somewhat complicated too, but can hopefully be tackled incrementally as well.

@cowtowncoder
Copy link
Member Author

Ok some more progress: now BasicDeserializerFactory actually uses primary Properties-based Creator discovered by POJOPropertiesCollector. Additionally POJOPropertiesCollector keeps Creator-properties from that primary one better in-sync with getter/setter/field accessor info (in fact making failing test for #4119 pass -- but will not yet marked as fixed until getting further with refactoring).

But quite a bit remains to rewrite in BasicDeserializerFactory still.

@cowtowncoder
Copy link
Member Author

cowtowncoder commented May 30, 2024

@JooHyukKim one quick note: while fixing issues with jackson-databind/3.0, I backported them into 2.x -- basically these are uncovered in 3.0 since it included parameter names module (unlike 2.x where it is separate). So there's a slight chance that something wrt Kotlin could be fixed by these changes.

@JooHyukKim
Copy link
Member

Kotlin module still failing. So I filed a PR moving the test to failing FasterXML/jackson-module-kotlin#802.

@StefanBratanov
Copy link

Hi, just to confirm after release 2.18 we would no longer need the following workaround that I had to use in one of my projects - https://stackoverflow.com/questions/68394911/why-record-class-cant-be-properly-deserialized-with-jackson/68998917#68998917 ?

@JooHyukKim
Copy link
Member

@StefanBratanov Probably. You are free to test ur case against Jackson 2.18 👍🏼.

In case it doesn't, let us know

@yihtserns
Copy link
Contributor

yihtserns commented Jun 13, 2024

@StefanBratanov I think your use case is already supported since 2.14 (see #2992 that was referenced in the StackOverflow answer).

carterkozak added a commit to carterkozak/jackson-databind that referenced this issue Sep 4, 2024
…legating JsonCreator

The refactor to bean property introspection in FasterXML#4515 caused
existing no-arg delegatring constructors to fail deserialization.

Example from this branch where we test RC releases:
palantir/conjure-java#2349
specifically this test:
https://github.com/palantir/conjure-java/blob/19d7f54eb0001c49b5d8a4bb897b9ad4cb5a28de/conjure-java-core/src/test/java/com/palantir/conjure/java/types/NoFieldBeanTests.java#L36-L39
Using this type:
https://github.com/palantir/conjure-java/blob/19d7f54eb0001c49b5d8a4bb897b9ad4cb5a28de/conjure-java-core/src/integrationInput/java/com/palantir/product/EmptyObjectExample.java
```
InvalidDefinitionException: Invalid type definition for type `com.palantir.product.EmptyObjectExample`: No argument left as delegating for Creator [method com.palantir.product.EmptyObjectExample#of()]: exactly one required
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]
	at app//com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
	at app//com.fasterxml.jackson.databind.DeserializationContext.reportBadTypeDefinition(DeserializationContext.java:1866)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitDelegatingCreator(BasicDeserializerFactory.java:489)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitDelegatingCreators(BasicDeserializerFactory.java:365)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:270)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:219)
	at app//com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:262)
	at app//com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:151)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:471)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:415)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:317)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:284)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:174)
	at app//com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:669)
	at app//com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:5048)
	at app//com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4918)
	at app//com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860)
	at app//com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3828)
	at app//com.palantir.conjure.java.types.NoFieldBeanTests.testDeserializeUsesSingleton(NoFieldBeanTests.java:38)
```

It's not entirely clear that we are correctly using the `DELEGATING` type here,
perhaps the `PROPERTIES` type would be a better fit, however neither necessarily
document support for a no-arg constructor. In general we prefer `DELEGATING`
when possible to avoid ambiguity with potential property sources -- we want
the ability to fail deserialization in this case when any properties are included
in the incoming object.

In 2.17.2, this branch allowed the code to functiona as we desired:
https://github.com/FasterXML/jackson-databind/blob/091d968751ef00150d22a788fe7f45b7cdcb337a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java#L658-L662

Is there any chance we could allow the previous DELEGATING behavior to continue
to handle the no-arg case? If we'd prefer to move away from `DELEGATING` for that
case, is there any chance we could emit a deprecation warning for some time?

I've included a potential patch which recovers the previous behavior.
carterkozak added a commit to carterkozak/jackson-databind that referenced this issue Sep 4, 2024
…legating JsonCreator

The refactor to bean property introspection in FasterXML#4515 caused
existing no-arg delegatring constructors to fail deserialization.

Example from this branch where we test RC releases:
palantir/conjure-java#2349
specifically this test:
https://github.com/palantir/conjure-java/blob/19d7f54eb0001c49b5d8a4bb897b9ad4cb5a28de/conjure-java-core/src/test/java/com/palantir/conjure/java/types/NoFieldBeanTests.java#L36-L39
Using this type:
https://github.com/palantir/conjure-java/blob/19d7f54eb0001c49b5d8a4bb897b9ad4cb5a28de/conjure-java-core/src/integrationInput/java/com/palantir/product/EmptyObjectExample.java
```
InvalidDefinitionException: Invalid type definition for type `com.palantir.product.EmptyObjectExample`: No argument left as delegating for Creator [method com.palantir.product.EmptyObjectExample#of()]: exactly one required
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]
	at app//com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
	at app//com.fasterxml.jackson.databind.DeserializationContext.reportBadTypeDefinition(DeserializationContext.java:1866)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitDelegatingCreator(BasicDeserializerFactory.java:489)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitDelegatingCreators(BasicDeserializerFactory.java:365)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:270)
	at app//com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:219)
	at app//com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:262)
	at app//com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:151)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:471)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:415)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:317)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:284)
	at app//com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:174)
	at app//com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:669)
	at app//com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:5048)
	at app//com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4918)
	at app//com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860)
	at app//com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3828)
	at app//com.palantir.conjure.java.types.NoFieldBeanTests.testDeserializeUsesSingleton(NoFieldBeanTests.java:38)
```

It's not entirely clear that we are correctly using the `DELEGATING` type here,
perhaps the `PROPERTIES` type would be a better fit, however neither necessarily
document support for a no-arg constructor. In general we prefer `DELEGATING`
when possible to avoid ambiguity with potential property sources -- we want
the ability to fail deserialization in this case when any properties are included
in the incoming object.

In 2.17.2, this branch allowed the code to functiona as we desired:
https://github.com/FasterXML/jackson-databind/blob/091d968751ef00150d22a788fe7f45b7cdcb337a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java#L658-L662

Is there any chance we could allow the previous DELEGATING behavior to continue
to handle the no-arg case? If we'd prefer to move away from `DELEGATING` for that
case, is there any chance we could emit a deprecation warning for some time?

I've included a potential patch which recovers the previous behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants