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

Fix tail-recursive deserializer (Lagom's #3241) #10840

Merged
merged 5 commits into from
Jul 5, 2021

Conversation

ignasi35
Copy link
Member

@ignasi35 ignasi35 commented May 3, 2021

Fixes JsonNodeDesrializer. See lagom/lagom#3241 for context and lagom/lagom#3241 (comment) for a workaround.

When tail recursing, the previous implementation would advance the cursor even when the recursion was about to complete. As a consequence, when invoking Jackson's readValue (or readValueAsTree, or any equivalent) from within a deserialization the cursor would skip some events leading to skipped properties and data loss.

@ignasi35 ignasi35 marked this pull request as draft May 3, 2021 11:04
@ignasi35 ignasi35 changed the title Prepare a reproducer for Lagom's #3241 Fix tail-recursive deserializer (Lagom's #3241) May 3, 2021
@ignasi35 ignasi35 marked this pull request as ready for review May 3, 2021 16:11
@ignasi35 ignasi35 requested a review from jrudolph May 3, 2021 16:11
@ignasi35
Copy link
Member Author

ignasi35 commented May 3, 2021

@Mergifyio backport 2.8.x

@mergify
Copy link
Contributor

mergify bot commented May 3, 2021

Command backport 2.8.x: pending

Waiting for the pull request to get merged

maybeValue match {
case Some(v) if nextContext.isEmpty =>
// done, no more tokens and got a value!
// note: jp.getCurrentToken == null happens when using treeToValue (we're not parsing tokens)
v

case maybeValue =>
// Read ahead
jp.nextToken()
Copy link
Member Author

Choose a reason for hiding this comment

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

Moving the nextToken() into this case is the fix.


@Override
public Child deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode node = jp.readValueAsTree();
Copy link
Member Author

Choose a reason for hiding this comment

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

Using jp.readValueAsTree() is necessary to reproduce the error.

It's in this invocation where the cursor ends up corrupted. When entering the invocation, jp is pointing too far away. Imagine we're parsing the following String:

  """
          |{
          |  "createdAt": 1234,
          |  "child": {
          |    "updatedAt": 555,
          |    "updatedBy": "another-user"
          |  },  // expected position of the cursor
          |  "updatedBy":   /*actual position of the cursor */  "some-user",
          |  "updatedAt": 5678
          |}
          |""".stripMargin

Copy link
Member

@mkurz mkurz left a comment

Choose a reason for hiding this comment

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

Besides a typo which I pushed a commit for, I think I understood the problem and the fix and the tests looks good to me 👍

Copy link
Contributor

@jrudolph jrudolph left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -22,8 +22,8 @@
public Parent(
@JsonProperty("createdAt") @NonNull Long createdAt,
@JsonProperty("child") Child child,
@JsonProperty("udpatedAt") @NonNull Long updatedAt,
@JsonProperty("udpatedBy") @NonNull String updatedBy) {
@JsonProperty("updatedAt") @NonNull Long updatedAt,
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't that have been covered by the test?

Copy link
Member Author

Choose a reason for hiding this comment

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

IDK. I wonder if Jackson is clever enough to try both the name on @JsonProperty and the field name.

Otherwise, it's possible the name in the JsonProperty annotation is not being used. I'll have a look and remove the annotations if they're not necessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is nuts! The following code which uses named properties but all names are wrong still makes the test pass:

  @JsonCreator
  public Parent(
      @JsonProperty("a45") @NonNull Long createdAt,
      @JsonProperty("a42") Child child,
      @JsonProperty("foo") @NonNull Long updatedAt,
      @JsonProperty("bar") @NonNull String updatedBy)

But the following code which doesn't use the annotation, fails:

  @JsonCreator
  public Parent(@NonNull Long createdAt, 
    Child child,
    @NonNull Long updatedAt,
    @NonNull String updatedBy) {

I've even removed the setters from the class to make sure those were not being picked up by Jackson.

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't that have been covered by the test?

To be honest, I thought as well there is something fishy, however because the test failed as expected in 16d6339 and got fixed by 400f7a8 I thought this isn't really relevant for what the test is originally about.

IDK. I wonder if Jackson is clever enough to try both the name on @JsonProperty and the field name.

Jackson can't look at the parameters names, because we don't compile with the -parameters flag, which is needed to store parameter names into class files so reflection can make use of it.

Based on your crazy example Ignasi, I went even further and tried:

  @JsonCreator
  public Parent(
      @JsonProperty("a45") @NonNull Long a,
      @JsonProperty("a42") Child b,
      @JsonProperty("foo") @NonNull Long c,
      @JsonProperty("bar") @NonNull String d) {
    this.createdAt = a;
    this.child = b;
    this.updatedAt = c;
    this.updatedBy = d;
  }

So not even the parameters names match. As expected even this passes the test.

My understanding of how Jackson works is that you have to mark parameters with @JsonProperty so they are taken into account. If a jackson property name can not be found in the actual json I guess Jackson tries to figure out the best matching json node, properly based on the order of the json properties. I googled a bit and it looks like order is somehow important.

Anyway, I think this Jackson magic is not really relevant for the original bug fix, so LGTM.

@ignasi35
Copy link
Member Author

ignasi35 commented May 5, 2021

I've reviewed the implementations of Parent and Child to make sure Jackson uses the constructor arguments (removed setters, made fields private and final, etc...).

I've also removed the JsonProperty annotation from the Child since those are not required because the Child class uses a custom serializer.

@ignasi35 ignasi35 requested a review from mkurz May 5, 2021 10:48
@ignasi35 ignasi35 merged commit f354f6c into master Jul 5, 2021
@ignasi35 ignasi35 deleted the undo-custom-json-parsing branch July 5, 2021 09:08
@mergify
Copy link
Contributor

mergify bot commented Jul 5, 2021

Command backport 2.8.x: success

Backports have been created

mergify bot added a commit that referenced this pull request Jul 9, 2021
Fix tail-recursive deserializer (Lagom's #3241) (backport #10840)
@mkurz mkurz added this to the 2.8.9 milestone Nov 26, 2021
@octonato octonato modified the milestones: 2.8.9, 2.8.11 Dec 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants