Skip to content
This repository was archived by the owner on Nov 7, 2019. It is now read-only.

Non-default property naming strategy w/o @JsonCreator leads to "Unrecognized field" #18

Closed
michaelhixson opened this issue May 27, 2015 · 10 comments

Comments

@michaelhixson
Copy link

I'm trying to use the feature of the parameter names module where I can leave out @JsonCreator and @JsonProperty annotations. This works great, except when I use a different naming strategy such as CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES. In that case I do need the @JsonCreator or else I get an error. This is with Jackson 2.6.0-rc1.

Would it be possible to make @JsonCreator optional here?

Here is some code demonstrating the issue:

package example;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

public class Example {
  static class Foo {
    Foo(int aA, int bB, int cC) {
      System.out.println(aA + ", " + bB + ", " + cC);
    }
  }

  static class Bar {
    @JsonCreator
    Bar(int aA, int bB, int cC) {
      System.out.println(aA + ", " + bB + ", " + cC);
    }
  }

  public static void main(String[] args) {
    String json1 = "{\"aA\":1,\"bB\":2,\"cC\":3}";
    String json2 = "{\"a_a\":1,\"b_b\":2,\"c_c\":3}";
    ObjectMapper mapper1 = new ObjectMapper()
        .registerModule(new ParameterNamesModule());
    ObjectMapper mapper2 = new ObjectMapper()
        .registerModule(new ParameterNamesModule())
        .setPropertyNamingStrategy(
            PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
    try {
      // This is ok
      mapper1.readValue(json1, Foo.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    try {
      // This throws
      mapper2.readValue(json2, Foo.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    try {
      // This is ok
      mapper1.readValue(json1, Bar.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    try {
      // This is ok
      mapper2.readValue(json2, Bar.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Here is the output of the above:

1, 2, 3
0, 0, 0
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "a_a" (class example.Example$Foo), not marked as ignorable (3 known properties: "aA", "bB", "cC"])
 at [Source: {"a_a":1,"b_b":2,"c_c":3}; line: 1, column: 26] (through reference chain: example.Foo["a_a"])
    at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:51)
    at com.fasterxml.jackson.databind.DeserializationContext.reportUnknownProperty(DeserializationContext.java:812)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:983)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1353)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperties(BeanDeserializerBase.java:1307)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:447)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1100)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:295)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:132)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3699)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2711)
    at example.Example.main(Example.java:39) <5 internal calls>
1, 2, 3
1, 2, 3
@cowtowncoder
Copy link
Member

Sounds like a bug; thank you for reporting and verifying with rc1. Most likely something is still unsynced: internally references to creator properties are kept in two separate places (map from name to property, creator instance with indexes) and it's possible one is modified but not the other.

@cowtowncoder
Copy link
Member

As per notes on the issue filed against jackson-databind, this is something that should work without @JsonCreator, but with current code does not and is not particularly easy to fix.
We hope to eventually fix it but until then, the workaround is unfortunately necessary.

@lpandzic
Copy link
Contributor

lpandzic commented Jun 5, 2015

Since the issue is in jackson-databind (both in code and on tracker), this issue should be closed.

@michaelhixson
Copy link
Author

No problem! This isn't blocking anything on my end. Please feel free to close - I'm following the other issue.

I'll share what my workaround was. There's a slim chance that someone out there is in my exact situation, where adding @JsonCreator is not ideal, and they eventually stumble across this Github issue.

I'm using a customized version of ParameterNamesModule. It's basically a copy, except in the NopAnnotationIntrospector I also override the hasCreatorAnnotation(Annotated) method to more aggresively return true. It checks that the argument is a constructor and that there is only one constructor in the declaring class. Obviously that won't work for everyone, but it happens to work in my codebase.

The reason I am so interested in getting rid of all @JsonCreator and @JsonProperty annotations on these constructors is that the classes are Lombok @Value classes. They have auto-generated constructors that don't appear in the source code. Those auto-generated constructors just so happen to name their parameters exactly after the fields. Using Lombok and the (modified) parameter names module, I can omit the constructors from my code entirely. This allowed me to remove a significant amount of samey boilerate code that looked like it was written by a robot.

Lombok offers another way I could work around this, which I avoided. It lets me append annotations to its auto-generated constructors:

@Value
@AllArgsConstructor(onConstructor=@__(@JsonCreator))
class SomeValue { ... }

That "works" in javac's eyes, but IDEs hate it (even with Lombok's IDE plugins), and I think I hate it too. So I went with the modified ParameterNamesModule.

@cowtowncoder
Copy link
Member

Thank you for sharing this. The work we have been doing has been aimed at eliminating the need, exactly like you describe. Besides Lombok, Scala module would really benefit from this.

So let's hope we can figure out how to get property information unified earlier to be available for renaming (or, perhaps if we must, perform late renaming for late-discovered creators).

@cowtowncoder
Copy link
Member

@michaelhixson Jackson 2.6 does use bit more heuristics in dealing with single-argument constructors. As long as a single-argument constructor is visible (for which you may need to change visibility settings, or use custom AnnotationIntrospector), mode detection is used. If explicit JsonCreator.Mode is defined with @JsonCreator, that is used. Otherwise:

  1. If an explicit name, or inject annotation used, consider it property-based
  2. If implicit name with matching getter or field (accessor) found, consider property-based
  3. Otherwise, consider it a delegating creator

Given this, it should be possible to find a way to avoid use of @JsonCreator annotation.
But some work is needed to provide information, such as implicit or explicit name for parameter for "hidden" constructor.

@michaelhixson
Copy link
Author

@cowtowncoder your previous comment was intended for #21, right?

If implicit name with matching getter or field (accessor) found, consider property-based

Just to clarify something about my example code in this issue and that other one:

I removed the fields and getters from the examples because I didn't realize they were relevant. My real code where I have these issues does have a field and standard getter method for each constructor param (though none of them are annotated).

@cowtowncoder
Copy link
Member

@michaelhixson Sorry, yes. Mostly. But it is actually bit related to this issue as well.

Alas, I found that there is a blocking issue in 2.6.0 which prevents detection, related to two-pass processing of things (first trying to find individual properties for getters/setters/creators; later on processing actual creators that class has, trying to reconcile).
So looks like there will be more refactoring with 2.7 to actually get things to click properly.

And that part is actually relevant here as well: unfortunately renaming occurs in the first phase, during which implicit creators have not been detected, and thereby properties (arguments) they have are not renamed either. Once that problem is resolved, this issue should get resolved. But until then, use of @JsonCreator or explicit naming with @JsonProperty will be needed.

@cowtowncoder
Copy link
Member

At this point not sure what remains, if anything, and there have been a few changes to jackson-databind, so will close this issue. Let's start with a new one if and when issues remain.

@cowtowncoder
Copy link
Member

Actually I think remaining work is captured under still open issue for databind:

FasterXML/jackson-databind#806

and there is a failing test.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants