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

one initial lower case named variables does not get properly serialized when named with JsonProperty #2868

Open
abrudin opened this issue Oct 2, 2020 · 4 comments

Comments

@abrudin
Copy link

abrudin commented Oct 2, 2020

Describe the bug
When using @JsonProperty("some_other_name") on a variable that starts with an initial lower case letter followed directly by an uppercase letter (for example aProp), the property gets duplicated when serializing the object. Once as "some_other_name", and once as "aprop". MapperFeature.USE_STD_BEAN_NAMING does not fix this, the only change is that the other property now is named AProp instead. The only way to fix this seems to be to name the getter getaProp(), which does not follow the Java Bean convention of capitalizing the first letter, and e.g. lombok does not do it this way.

Version information
Which Jackson version(s) was this for?
2.11.2

To Reproduce

public class JacksonTest {

  @Test
  void thisWorks() throws JsonProcessingException, JSONException {
    final String result = new ObjectMapper().writeValueAsString(new MyFineObject("hello"));
    System.out.println(result);
    // {"renamed": "hello"}
  }

  @Test
  void thisDoesnt() throws JsonProcessingException, JSONException {
    final String result = new ObjectMapper().writeValueAsString(new MyTroublesomeObject("hello"));
    System.out.println(result);
    // {"atest":"hello","renamed":"hello"}
  }

  @Test
  void noHelp() throws JsonProcessingException {
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
    final String result = objectMapper.writeValueAsString(new MyTroublesomeObject("hello"));
    System.out.println(result);
    // {"ATest":"hello","renamed":"hello"}
  }

  @Test
  void thisAlsoWorksButShouldItNeedToBeLikeThis() throws JsonProcessingException, JSONException {
    final String result =
        new ObjectMapper().writeValueAsString(new MyFixedTroublesomeObject("hello"));
    System.out.println(result);
    // {"renamed":"hello"}
  }

  static class MyFineObject {
    @JsonProperty("renamed")
    private final String test;

    public MyFineObject(final String test) {
      this.test = test;
    }

    public String getTest() {
      return test;
    }
  }

  static class MyTroublesomeObject {
    @JsonProperty("renamed")
    private final String aTest;

    public MyTroublesomeObject(final String aTest) {
      this.aTest = aTest;
    }

    public String getATest() {
      return aTest;
    }
  }

  static class MyFixedTroublesomeObject {
    @JsonProperty("renamed")
    private final String aTest;

    public MyFixedTroublesomeObject(final String aTest) {
      this.aTest = aTest;
    }

    public String getaTest() {
      return aTest;
    }
  }
}

Expected behavior
There should only be one property, "renamed".

Additional context
As we use lombok to create accessors, we cannot solve it in as in MyFixedTroublesomeObject above.

@abrudin abrudin added the to-evaluate Issue that has been received but not yet evaluated label Oct 2, 2020
@cowtowncoder
Copy link
Member

I am not sure there is a way to resolve this, generally: as far as I know, something like

getATest()

can only be mangled to either atest (old Jackson) or ATest (bean compatible). My understanding is that latter is what Bean specification would suggest -- however, if you or anyone else has a link to something that explains this is NOT how it should work, I'd be happy to have a look.
Originally change to "bean-compatible" was mostly to cover getURL() case (from "url" to "URL"), fwtw.

However: I am hoping to work bit more on #2624 (and esp. #2800) to allow ways for users to be able to customize handling.
For 3.0 I am also thinking about maybe allowing case-insensitive matching: this can be tricky (when casing does not match, which one to consider canonical casing for writing?) but might be the way to go.
It would probably be necessary to let field be the leading indicator, somehow, as that seems to be the common approach in contexts like Kotlin.

In the meantime the way to resolve it for specific case is to add @JsonProperty for both field and accessor -- this will link them to same logical property in case name mangling does not achieve that.

@abrudin
Copy link
Author

abrudin commented Oct 2, 2020

Yes, I understand that this is a hard nut to crack, but it very well may be that the case-insensitive strategy can be a solution as a setting on the objectMapper, maybe even with a setting (fromAccessors/fromFields) even if I'm unsure how compatible this would be with the current configuration system.
The thing for us is mostly that the combination lombok/jackson in this case clearly violates the principle of least surprise:

  @Test
  void lombokJackson() throws JsonProcessingException {
    final String result = new ObjectMapper().writeValueAsString(new MyLombokObject("hello"));
    System.out.println(result);
    // {"atest":"hello","renamed":"hello"}
  }

  @Value
  static class MyLombokObject {
    @JsonProperty("renamed")
    private final String aTest;
  }

And as you say and to correct my initial report, there are easy ways to work around the problem (add a getter with @JsonProperty or just avoid using aTest-like variable names), so the main issue is that the behavior may turn up without anyone noticing it until it is (potentially) too late. And I guess a case-insensitive strategy would only partly help with that as long as it not enabled by default. In our case though we are using an objectMapper which is configured identically in all our code bases, so it would be a step in the right direction to detect this once and then "fix" it on the mapper configuration level.

@cowtowncoder
Copy link
Member

The problem here is that the way Lombok typically works is not the way Jackson's handling has existed since the beginning -- it starts with field name, usually, trying to determine accessor name. Jackson starts with accessor name(s), trying to match that to field name.
This is tricky all around, as same/similar problem occurs with Kotlin as well (and probably Scala), so I am at least aware of it.

Jackson 2.12.0 will include #2800 (pluggable AccessorNamingStrategy) which should allow customizing handling to work the way you want, I think. I hope to release first release candidate very soon now.

@abrudin
Copy link
Author

abrudin commented Oct 4, 2020

Thank you for fast and excellent feedback, and for maintaining this excellent library. I'm looking forward to the 2.12.0 release!

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

No branches or pull requests

2 participants