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

JsonProperty not serializing field names properly on JsonCreator in record #4452

Open
1 task done
Incara opened this issue Mar 26, 2024 · 15 comments
Open
1 task done
Labels
2.17 Issues planned at earliest for 2.17 has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue

Comments

@Incara
Copy link

Incara commented Mar 26, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Serialization of java records do not appear to be honoring @JsonProperty names in @JsonCreator constructors.

Version Information

2.15.x+

Reproduction

A test below fails for me and I expect it to pass.

import static org.junit.Assert.assertTrue;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class RecordTest {

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public record TestObject(String testFieldName, Integer testOtherField) {

        @JsonCreator
        public TestObject(@JsonProperty ("strField") String testFieldName,
                          @JsonProperty ("intField") Integer testOtherField) {
            this.testFieldName = testFieldName;
            this.testOtherField = testOtherField;
        }
    }

    @Test
    public void test() throws JsonProcessingException {
        final String result = OBJECT_MAPPER.writeValueAsString(new TestObject("test", 1));
        assertTrue(result.contains("strField"));
    }
}

Expected behavior

The field is renamed to what is specified in the @JsonProperty annotation in the record's @JsonCreator

Additional context

We encountered this upgrading from 2.14.x up to 2.16.x, so this functionality was working at that earlier version (perhaps before records were really addressed).

Perhaps we have versions incorrect between jackson modules and libraries, but we haven't found the exact mismatch yet.

@Incara Incara added the to-evaluate Issue that has been received but not yet evaluated label Mar 26, 2024
@yihtserns
Copy link
Contributor

Tested using 2.14.2 - not working. But it's amazing that it ever worked at all because annotating on constructor arguments will only produce this compiled code:

public static record TestObject(String testFieldName, Integer testOtherField) {
    @JsonCreator
    public TestObject(@JsonProperty("strField") String testFieldName, @JsonProperty("intField") Integer testOtherField) {
        this.testFieldName = testFieldName;
        this.testOtherField = testOtherField;
    }

    public String testFieldName() {
        return this.testFieldName;
    }

    public Integer testOtherField() {
        return this.testOtherField;
    }
}

...the @JsonProperty annotation won't be propagated to the field & accessor method since Java doesn't know which constructor argument is associated with which field/accessor.

Only by annotating in the Record's header i.e.:

public static record TestObject(@JsonProperty("strField") String testFieldName, @JsonProperty("intField") Integer testOtherField) {
    @JsonCreator
    public TestObject(String testFieldName, Integer testOtherField) {
        this.testFieldName = testFieldName;
        this.testOtherField = testOtherField;
    }

    public String testFieldName() {
        return this.testFieldName;
    }

    public Integer testOtherField() {
        return this.testOtherField;
    }
}

...will a usable compiled code be produced:

public static record TestObject(String testFieldName, Integer testOtherField) {
    @JsonCreator
    public TestObject(String testFieldName, Integer testOtherField) {
        this.testFieldName = testFieldName;
        this.testOtherField = testOtherField;
    }

    @JsonProperty("strField")
    public String testFieldName() {
        return this.testFieldName;
    }

    @JsonProperty("intField")
    public Integer testOtherField() {
        return this.testOtherField;
    }
}

@Incara
Copy link
Author

Incara commented Mar 26, 2024

Yeah, I think we've figured out this test specifically doesn't work on any version. We're not sure why it works for us with our application's old dependency combination. I don't know if it's something about the parameter names module or properties on the spring ObjectMapper injected at runtime that allows it to be possible.

Is this something that is intended to be supported or is it a requirement that @JsonProperty annotations must appear in the record declaration?

@yihtserns
Copy link
Contributor

If the ObjectMapper is created with:

  • .registerModule(new ParameterNamesModule()), or
  • .findAndRegisterModules() with jackson-module-parameter-names in the classpath

...then it'll work but I think that's more of a happy coincidence that worked because the constructor argument names happen to match the property (field/accessor) names. And that's now broken in 2.16.0 (works OK in 2.15.4).

I'm not entirely sure, but I vaguely remember seeing an issue talking about names... But I'm not gonna dig further since I'm just here to make sure it wasn't my contribution from 1 year ago that caused this - imma leave this to the core maintainers.

🏃‍♀️💨

@Incara
Copy link
Author

Incara commented Mar 26, 2024

Much appreciated, this helps with analysis for sure. I think that narrows this down to something changed between 2.15.4 and 2.16.0 and now it doesn't seem like @JsonProperty renames are honored on @JsonCreator in records. The below works in 2.15.4 but fails in 2.16.0:

import static org.junit.Assert.assertTrue;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

public class RecordTest {

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .registerModule(new ParameterNamesModule()).findAndRegisterModules();

    public record TestObject(String testFieldName, Integer testOtherField) {

        @JsonCreator
        public TestObject(@JsonProperty ("strField") String testFieldName,
                          @JsonProperty ("intField") Integer testOtherField) {
            this.testFieldName = testFieldName;
            this.testOtherField = testOtherField;
        }
    }

    @Test
    public void test() throws JsonProcessingException {
        final String result = OBJECT_MAPPER.writeValueAsString(new TestObject("test", 1));
        assertTrue(result.contains("strField"));
    }
}

For now, our solution is to eliminate our @JsonCreators on records in favor using @JsonProperty directly on the record definition attributes since there is larger support in jackson for records since we last updated.

I don't know if there is supposed to be support for the above test case from the repo maintainers, but I will also stop and let them comment.

@JooHyukKim
Copy link
Member

JooHyukKim commented Mar 27, 2024

So as per release note there seem to be these three PRs --#3906, #3992, #4175 -- that are titled with the word record in it. Other than that, there is a long list of updates that got included in 2.16.0 release.

@yihtserns
Copy link
Contributor

...titled with the word record in it.

I don't think this thing is Record-specific.

@JooHyukKim
Copy link
Member

Because of project's current on-going milestone aka Property Introspection Rewrite (also mentioned in 2.17), we neither can have have fix any time soon nor confidently specify how record should behave.

@Incara
Copy link
Author

Incara commented Mar 28, 2024

Because of project's current on-going milestone aka Property Introspection Rewrite (also mentioned in 2.17), we neither can have have fix any time soon nor confidently specify how record should behave.

I think that's fine. The below test does work on 2.16.x and that suits our use cases for now.

import static org.junit.Assert.assertTrue;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

public class RecordTest {

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .registerModule(new ParameterNamesModule()).findAndRegisterModules();

    public record TestObject(@JsonProperty ("strField") String testFieldName,
                             @JsonProperty ("intField") Integer testOtherField) {
    }

    @Test
    public void test() throws JsonProcessingException {
        final String result = OBJECT_MAPPER.writeValueAsString(new TestObject("test", 1));
        assertTrue(result.contains("strField"));
    }
}

It's possible that this is the correct and only supported future usage, but there probably needs to be some consideration (an error, documentation, warning, etc) about the presence of a @JsonCreator in a record that modifies field names or changes the input of the record. Another example below:

import static org.junit.Assert.assertTrue;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

public class RecordTest {

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .registerModule(new ParameterNamesModule()).findAndRegisterModules();

    public record TestObject(String testFieldName, Integer testOtherField) {

        @JsonCreator
        public TestObject(@JsonProperty ("strField") String testFieldName,
                          @JsonProperty ("someOtherIntField") Integer testOtherIntField,
                          @JsonProperty ("intField") Integer testOtherField) {
            this(testFieldName, testOtherField + testOtherIntField);
        }
    }

    @Test
    public void test() throws JsonProcessingException {
        final String result = OBJECT_MAPPER.writeValueAsString(new TestObject("test", 2, 1));
        assertTrue(result.contains("intField"));
        assertTrue(result.contains("strField"));
    }
}

I'm not even sure how the above could work, so it's most likely not valid or going to be supported, but I don't know that as a consumer.

@cowtowncoder
Copy link
Member

FWTW, if not Record-specific, reproduction without Record would be great (since it's likely in shared code b/w POJOs, Records).

@cowtowncoder
Copy link
Member

Looking at the original case, it does seem like a flaw: @JsonCreator-annotated constructor should be used as the primary one, and any property name changes should be used from that.

And while fix may or may not be easy, a PR for reproduction, would be great.
It seems to be this would be Record-specific thing, fwtw, as fully annotated case

...titled with the word record in it.

I don't think this thing is Record-specific.

Looking at the original reproduction, it would seem it is?
Or why do you think it isn't?

@cowtowncoder
Copy link
Member

Ok, at any rate: a PR for failing test case (under src/test-jdk17/java/..../failing) would be great, either against 2.17 or 2.18 branch.

@JooHyukKim
Copy link
Member

Ok, at any rate: a PR for failing test case (under src/test-jdk17/java/..../failing) would be great, either against 2.17 or 2.18 branch.

Most test cases here make use of ParameterNamesModule, so it seems not possible?

@yihtserns
Copy link
Contributor

@cowtowncoder
Copy link
Member

Ugh. I missed ParameterNamesModule reference since that wasn't in the original description.

Correct, tests here cannot use it.

There are a few tests tho that implement replica, AnnotationIntrospector that provides bogus names for testing. So it is possible, if tedious, to do that (beside one that @yihtserns referenced).

@JooHyukKim
Copy link
Member

Filed PR #4477. It does fail from 2.16 version and on.

@cowtowncoder cowtowncoder added 2.17 Issues planned at earliest for 2.17 and removed to-evaluate Issue that has been received but not yet evaluated labels Apr 16, 2024
cowtowncoder pushed a commit that referenced this issue Apr 16, 2024
@cowtowncoder cowtowncoder added the has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue label Apr 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.17 Issues planned at earliest for 2.17 has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue
Projects
None yet
Development

No branches or pull requests

4 participants