How to deserialize a class with overloaded constructors using JsonCreator

JavaJsonJackson

Java Problem Overview


I am trying to deserialize an instance of this class using Jackson 1.9.10:

public class Person {

@JsonCreator
public Person(@JsonProperty("name") String name,
		@JsonProperty("age") int age) {
	// ... person with both name and age
}

@JsonCreator
public Person(@JsonProperty("name") String name) {
	// ... person with just a name
}
}

When I try this I get the following

> Conflicting property-based creators: already had ... {interface org.codehaus.jackson.annotate.JsonCreator @org.codehaus.jackson.annotate.JsonCreator()}], encountered ... , annotations: {interface org.codehaus.jackson.annotate.JsonCreator @org.codehaus.jackson.annotate.JsonCreator()}]

Is there a way to deserialize a class with overloaded constructors using Jackson?

Thanks

Java Solutions


Solution 1 - Java

Though its not properly documented, you can only have one creator per type. You can have as many constructors as you want in your type, but only one of them should have a @JsonCreator annotation on it.

Solution 2 - Java

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<String, Object> 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<String, Object> properties), which means the Phone(@JsonProperty("value") String value) is never used.

Solution 3 - Java

If I got right what you are trying to achieve, you can solve it without a constructor overload.

If you just want to put null values in the attributes not present in a JSON or a Map you can do the following:

@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {
    private String name;
    private Integer age;
    public static final Integer DEFAULT_AGE = 30;

    @JsonCreator
    public Person(
        @JsonProperty("name") String name,
        @JsonProperty("age") Integer age) 
        throws IllegalArgumentException {
        if(name == null)
            throw new IllegalArgumentException("Parameter name was not informed.");
        this.age = age == null ? DEFAULT_AGE : age;
        this.name = name;
    }
}

That was my case when I found your question. It took me some time to figure out how to solve it, maybe that's what you were tring to do. @Brice solution did not work for me.

Solution 4 - Java

If you don't mind doing a little more work, you can deserialize the entity manually:

@JsonDeserialize(using = Person.Deserializer.class)
public class Person {

	public Person(@JsonProperty("name") String name,
			@JsonProperty("age") int age) {
		// ... person with both name and age
	}

	public Person(@JsonProperty("name") String name) {
		// ... person with just a name
	}
	
	public static class Deserializer extends StdDeserializer<Person> {
		public Deserializer() {
			this(null);
		}
		
		Deserializer(Class<?> vc) {
			super(vc);
		}

		@Override
		public Person deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
			JsonNode node = jp.getCodec().readTree(jp);
			if (node.has("name") && node.has("age")) {
				String name = node.get("name").asText();
				int age = node.get("age").asInt();
				return new Person(name, age);
			} else if (node.has("name")) {
				String name = node.get("name").asText();
				return new Person("name");
			} else {
				throw new RuntimeException("unable to parse");
			}
		}
	}
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiongeejayView Question on Stackoverflow
Solution 1 - JavaPerceptionView Answer on Stackoverflow
Solution 2 - JavaBriceView Answer on Stackoverflow
Solution 3 - JavaTiago Stapenhorst MartinsView Answer on Stackoverflow
Solution 4 - JavaMalcolm CrumView Answer on Stackoverflow