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

Change Records deserialization to use the same mechanism as POJO deserialization. #3724

Merged
merged 2 commits into from
Jan 14, 2023

Conversation

yihtserns
Copy link
Contributor

@yihtserns yihtserns commented Jan 9, 2023

New

  • Supports @JsonCreator.mode=DISABLED on canonical constructor.
  • Supports deserialization using "implicit" (i.e. without @JsonCreator annotation) non-canonical constructor when 1/more of its parameter is/are annotated with @JsonProperty.
  • Supports "implicit" (i.e. without @JsonCreator annotation) 1-arg delegating constructor.
  • Supports "implicit" (i.e. without @JsonCreator annotation) 1-arg delegating factory method.
  • Supports disabling "implicit" (i.e. without @JsonCreator annotation) constructor/factory method detection via MapperFeature.AUTO_DETECT_CREATORS.

This is a non-exhaustive list - I don't have full knowledge of existing JavaBeans deserialization capabilities.

Fixes

@yihtserns yihtserns changed the base branch from 2.15 to 2.14 January 9, 2023 12:03
public static AnnotatedConstructor findRecordConstructor(DeserializationContext ctxt,
BeanDescription beanDesc, List<String> names) {
return new CreatorLocator(ctxt, beanDesc)
public static AnnotatedConstructor findRecordConstructor(AnnotatedClass recordClass, AnnotationIntrospector intr, MapperConfig<?> config, List<String> names) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API change? Possibly not a good thing - any chance that the old version can be kept and deprecated?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no, please leave the signature as it was: DeserializationContext and BeanDescription can be used to access AnnotationIntrospector and MapperConfig.

Copy link
Contributor Author

@yihtserns yihtserns Jan 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API change? Possibly not a good thing - any chance that the old version can be kept and deprecated?

Done - added a new method while keeping (but not deprecating) the old method.

Yeah no, please leave the signature as it was: DeserializationContext and BeanDescription can be used to access AnnotationIntrospector and MapperConfig.

The problem is in the place I'm using it now (POJOPropertiesCollector), I don't have DeserializationContext nor BeanDescription...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, ok that makes sense. But that's bit of a problem. Let me think about it.

@cowtowncoder
Copy link
Member

First of all, thank you for contributing this @yihtserns ! It sounds like solid improvement, potentially.

I added some smaller notes on keeping existing API in some places, but there is one bigger request I have: this PR needs to be against 2.15 branch; I don't feel comfortable merging it in a patch to 2.14 -- patches are typically for smaller, less risky changes, and while I don't have specific issues here I feel there is potential for some breakage.

@yihtserns
Copy link
Contributor Author

...this PR needs to be against 2.15 branch; I don't feel comfortable merging it in a patch to 2.14...

Do I put "since 2.15" in the deprecation notice?

And yeah I wasn't sure whether I should go with 2.14 or 2.15 since https://github.com/FasterXML/jackson/blob/master/CONTRIBUTING.md didn't mention the latter... That doc needs to be updated?

@cowtowncoder
Copy link
Member

...this PR needs to be against 2.15 branch; I don't feel comfortable merging it in a patch to 2.14...

Do I put "since 2.15" in the deprecation notice?

And yeah I wasn't sure whether I should go with 2.14 or 2.15 since https://github.com/FasterXML/jackson/blob/master/CONTRIBUTING.md didn't mention the latter... That doc needs to be updated?

...this PR needs to be against 2.15 branch; I don't feel comfortable merging it in a patch to 2.14...

Do I put "since 2.15" in the deprecation notice?

Yes.

And yeah I wasn't sure whether I should go with 2.14 or 2.15 since https://github.com/FasterXML/jackson/blob/master/CONTRIBUTING.md didn't mention the latter... That doc needs to be updated?

Thanks, I'll need to update that. There are some things that should go into earlier branches too -- generally earliest maintained, for safe enough fixes. So it is not easy to correctly guess what I think is the right branch :)
(which is also why I don't mind pointing out what I think should be used).

@@ -467,6 +467,10 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
// regular property? needs buffering
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) {
if (prop.isIgnorable() && _beanType.isRecordType()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a problem here, to require additional processing? And/or would this work even for POJO types?

@cowtowncoder
Copy link
Member

Ok, looks pretty good. I do need to go over it again with more thought, but I like how small changes are.

@yihtserns yihtserns changed the title Change Records deserialization to use the same mechanism as JavaBeans deserialization. Change Records deserialization to use the same mechanism as POJO deserialization. Jan 12, 2023
@cowtowncoder cowtowncoder added the cla-needed PR looks good (although may also require code review), but CLA needed from submitter label Jan 14, 2023
@cowtowncoder
Copy link
Member

@yihtserns One more thing: I hope to be able to merge this soon. One last thing before that is that we need a Contributor License Agreement (CLA) before merging the first contribution. This only needs to be done once and then it is good for all future PRs for Jackson project.

Document is here:

https://github.com/FasterXML/jackson/blob/master/contributor-agreement.pdf

and the usual way is to print, fill & sign, scan/photo, email to info at fasterxml dot com.
Once I get the document I will be able to merge the PR (I will do one more code review but I assume we are very close).

Thank you again for providing this patch -- looking forward to merging it!

@yihtserns
Copy link
Contributor Author

yihtserns commented Jan 14, 2023

I'm currently re-reviewing if @JsonIgnore can be supported in a different way (else the explanation of the current approach will be a headache).

UPDATE: Didn't manage to find a low-change way, so decided to stick with this approach (with some difference) + smothered it with docs.

@yihtserns
Copy link
Contributor Author

yihtserns commented Jan 14, 2023

One last thing before that is that we need a Contributor License Agreement (CLA) before merging the first contribution

CLA sent.

@cowtowncoder cowtowncoder removed the cla-needed PR looks good (although may also require code review), but CLA needed from submitter label Jan 14, 2023
@yihtserns
Copy link
Contributor Author

@leonardehrenfried that symptom sounds similar to the one describe in #3628 (comment) - let me test that.

@yihtserns
Copy link
Contributor Author

@leonardehrenfried so I tested OpenTripPlanner:

  1. Changed jackson.version in pom.xml to 2.15.0.
  2. Run mvn test to observe the test failures (there's 1 unrelated to Records, so I'm ignoring it).
  3. Create a local fork of jackson-databind:2.15.0, reverting 0a4cfc4.
  4. Change OpenTripPlanner/pom.xml's jackson-databind to refer to that local fork.
  5. Run mvn test again - no more test failures related to Records.

I've confirmed that the issue you're facing is related to the broken workaround mentioned in #3628, unrelated to this change.

@leonardehrenfried
Copy link

I since checked and we seem to be using a custom object mapper in the test:

      objectMapper = new ObjectMapper();
      objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
      objectMapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
      objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
      objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
      objectMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
      objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
      objectMapper.registerModule(new JavaTimeModule());
      objectMapper.registerModule(new Jdk8Module());

      objectMapper.setVisibility(
        objectMapper
          .getSerializationConfig()
          .getDefaultVisibilityChecker()
          .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
          .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
          .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
          .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
      );

https://github.com/opentripplanner/OpenTripPlanner/blob/dev-2.x/src/test/java/org/opentripplanner/routing/algorithm/mapping/SnapshotTestBase.java#L366-L384

So it appears that your analysis is totally correct.

I'm still not clear if this is was an intentional change in Jackson or not - can you clarify this for me?

Asked the other way around: is the old behaviour going to be restored and I should just wait for a new release or do I need to change the config of my object mapper?

@yihtserns
Copy link
Contributor Author

...do I need to change the config of my object mapper?

I think that config was used because you had the same problem as #3628, and I can't think of an alternative solution for that problem.

...is the old behaviour going to be restored and I should just wait for a new release...

I've helped create #3894 to support that type of config.

@cowtowncoder
Copy link
Member

Renovate auto-updated our application to Jackson 2.15 tonight. Some tests which check that records can be serialized fail as records are always serialized to empty objects.

These are plain records with no annotations or methods. Should that just work or do I need to configure/annotate something? In Jackson 2.14 it just worked.

This is why we publish release candidates -- 2.15.0-rc1, rc2, rc3 -- to try to get users to verify if they find any regressions beyond what our unit test suite catches. Nothing was reported for this.

Now, I don't know what tests you have so I cannot say if behavior is expected or not: sometimes usage is based on undefined behavior, in which case change may be intended (typically due to a fix to some known problem, and change to unsupported case incidental).

Thank you for helping create a new issue. We'll see where that leads.

@leonardehrenfried
Copy link

I'm sorry and as an open source maintainer myself I feel you.

I'm the last guy showing up on an issue making demands.

I would love to help diagnose and test and don't expect you to make a potentially broken config work for my benefit.

@cowtowncoder
Copy link
Member

@leonardehrenfried Oh and just to make sure: I did not mean to blame you or users at all. I appreciate your reaching out and reporting the issue. I think we can figure this out; PR submitted might be the way, although I have some reservations. But it's a tricky problem.

@yihtserns
Copy link
Contributor Author

yihtserns commented Apr 24, 2023

@leonardehrenfried I've spent a bit of time to study your codebase to understand why that config was used - seems like it was to prevent ApiLeg.getDuration() from being serialized.

I assume that is to make assertion easier, since that method returns non-constant number?

If that's the case, here's my suggestion (tested with 2.15.0):

public abstract class SnapshotTestBase {
    ...

    private static class SnapshotItinerarySerializer implements SnapshotSerializer {

        private SnapshotItinerarySerializer() {
            ...

            // Remove this config:
            //objectMapper.setVisibility(
            //    objectMapper
            //      .getSerializationConfig()
            //      .getDefaultVisibilityChecker()
            //      .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
            //      .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
            //      .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
            //      .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
            //);

            // Add this:
            objectMapper.addMixIn(ApiLeg.class, ApiLegMixin.class);
            ...
        }
    }

    
    /**
     * To exclude {@link ApiLeg#getDuration()} from being deserialized because the returned number
     * is non-constant making it impossible to assert.
     */
    private abstract static class ApiLegMixin {

      @JsonIgnore
      abstract double getDuration();
    }
}

@leonardehrenfried
Copy link

@yihtserns thanks so much for this. It absolutely fixes the problems we are having. Fantastic work!

@agavrilov76
Copy link
Contributor

Related to #3900

@cowtowncoder
Copy link
Member

Another breakage: #3938.

@yihtserns
Copy link
Contributor Author

I used to think "nobody would ever put a setter in a Record since nothing is mutable, right?"

Now I know how ignorant I was...

@cowtowncoder
Copy link
Member

To be honest, that use case is a bit "Creative" and just used to prevent deserialization of something without full ignoral.
But it is something I'd have used with regular POJOs and works very well for what is needed.

If someone else has shown that use case to me, I might have argued against it tho, with Records. Given that they are immutable.

Anyway, was happy to figure out how to make it work again via fix for #3928.

@yihtserns
Copy link
Contributor Author

Caused #3968.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants