How to deserialize a class with overloaded constructors using JsonCreator

后端 未结 4 1412
挽巷
挽巷 2020-12-08 18:24
4条回答
  •  伪装坚强ぢ
    2020-12-08 18:40

    EDIT: Behold, in a blog post by the maintainers of Jackson, it seems 2.12 may see improvements in regard to constructor injection. (Current version at the time of this edit is 2.11.1)

    Improve auto-detection of Constructor creators, including solving/alleviating issues with ambiguous 1-argument constructors (delegating vs properties)


    This still hold true for Jackson databind 2.7.0.

    The Jackson @JsonCreator annotation 2.5 javadoc or Jackson annotations documentation grammar (constructors and factory methods) let believe indeed that one can mark multiple constructors.

    Marker annotation that can be used to define constructors and factory methods as one to use for instantiating new instances of the associated class.

    Looking at the code where the creators are identified, it looks like the Jackson CreatorCollector is ignoring overloaded constructors because it only checks the first argument of the constructor.

    Class oldType = oldOne.getRawParameterType(0);
    Class newType = newOne.getRawParameterType(0);
    
    if (oldType == newType) {
        throw new IllegalArgumentException("Conflicting "+TYPE_DESCS[typeIndex]
               +" creators: already had explicitly marked "+oldOne+", encountered "+newOne);
    }
    
    • oldOne is the first identified constructor creator.
    • newOne is the overloaded constructor creator.

    That means that code like that won't work

    @JsonCreator
    public Phone(@JsonProperty("value") String value) {
        this.value = value;
        this.country = "";
    }
    
    @JsonCreator
    public Phone(@JsonProperty("country") String country, @JsonProperty("value") String value) {
        this.value = value;
        this.country = country;
    }
    
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336"); // raise error here
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
    

    But this code will work :

    @JsonCreator
    public Phone(@JsonProperty("value") String value) {
        this.value = value;
        enabled = true;
    }
    
    @JsonCreator
    public Phone(@JsonProperty("enabled") Boolean enabled, @JsonProperty("value") String value) {
        this.value = value;
        this.enabled = enabled;
    }
    
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\",\"enabled\":true}", Phone.class).value).isEqualTo("+336");
    

    This is a bit hacky and may not be future proof.


    The documentation is vague about how object creation works; from what I gather from the code though, it's that it is possible to mix different methods :

    For example one can have a static factory method annotated with @JsonCreator

    @JsonCreator
    public Phone(@JsonProperty("value") String value) {
        this.value = value;
        enabled = true;
    }
    
    @JsonCreator
    public Phone(@JsonProperty("enabled") Boolean enabled, @JsonProperty("value") String value) {
        this.value = value;
        this.enabled = enabled;
    }
    
    @JsonCreator
    public static Phone toPhone(String value) {
        return new Phone(value);
    }
    
    assertThat(new ObjectMapper().readValue("\"+336\"", Phone.class).value).isEqualTo("+336");
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\",\"enabled\":true}", Phone.class).value).isEqualTo("+336");
    

    It works but it is not ideal. In the end, it could make sense, e.g. if the JSON is that dynamic then maybe one should look to use a delegate constructor to handle payload variations much more elegantly than with multiple annotated constructors.

    Also note that Jackson orders creators by priority, for example in this code :

    // Simple
    @JsonCreator
    public Phone(@JsonProperty("value") String value) {
        this.value = value;
    }
    
    // more
    @JsonCreator
    public Phone(Map properties) {
        value = (String) properties.get("value");
        
        // more logic
    }
    
    assertThat(new ObjectMapper().readValue("\"+336\"", Phone.class).value).isEqualTo("+336");
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
    assertThat(new ObjectMapper().readValue("{\"value\":\"+336\",\"enabled\":true}", Phone.class).value).isEqualTo("+336");
    

    This time Jackson won't raise an error, but Jackson will only use the delegate constructor Phone(Map properties), which means the Phone(@JsonProperty("value") String value) is never used.

提交回复
热议问题