How to de/serialize an immutable object without default constructor using ObjectMapper?

JavaJson

Java Problem Overview


I want to serialize and deserialize an immutable object using com.fasterxml.jackson.databind.ObjectMapper.

The immutable class looks like this (just 3 internal attributes, getters and constructors):

public final class ImportResultItemImpl implements ImportResultItem {

	private final ImportResultItemType resultType;

	private final String message;

	private final String name;

	public ImportResultItemImpl(String name, ImportResultItemType resultType, String message) {
		super();
		this.resultType = resultType;
		this.message = message;
		this.name = name;
	}

	public ImportResultItemImpl(String name, ImportResultItemType resultType) {
		super();
		this.resultType = resultType;
		this.name = name;
		this.message = null;
	}

	@Override
	public ImportResultItemType getResultType() {
		return this.resultType;
	}

	@Override
	public String getMessage() {
		return this.message;
	}

	@Override
	public String getName() {
		return this.name;
	}
}

However when I run this unit test:

@Test
public void testObjectMapper() throws Exception {
	ImportResultItemImpl originalItem = new ImportResultItemImpl("Name1", ImportResultItemType.SUCCESS);
	String serialized = new ObjectMapper().writeValueAsString((ImportResultItemImpl) originalItem);
	System.out.println("serialized: " + serialized);

    //this line will throw exception
	ImportResultItemImpl deserialized = new ObjectMapper().readValue(serialized, ImportResultItemImpl.class);
}

I get this exception:

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class eu.ibacz.pdkd.core.service.importcommon.ImportResultItemImpl]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: {"resultType":"SUCCESS","message":null,"name":"Name1"}; line: 1, column: 2]
	at 
... nothing interesting here

This exception asks me to create a default constructor, but this is an immutable object, so I don't want to have it. How would it set the internal attributes? It would totally confuse the user of the API.

So my question is: Can I somehow de/serialize immutable objects without default constructor?

Java Solutions


Solution 1 - Java

To let Jackson know how to create an object for deserialization, use the @JsonCreator and @JsonProperty annotations for your constructors, like this:

@JsonCreator
public ImportResultItemImpl(@JsonProperty("name") String name, 
        @JsonProperty("resultType") ImportResultItemType resultType, 
        @JsonProperty("message") String message) {
    super();
    this.resultType = resultType;
    this.message = message;
    this.name = name;
}

Solution 2 - Java

You can use a private default constructor, Jackson will then fill the fields via reflection even if they are private final.

EDIT: And use a protected/package-protected default constructor for parent classes if you have inheritance.

Solution 3 - Java

The first answer of Sergei Petunin is right. However, we could simplify code with removing redundant @JsonProperty annotations on each parameter of constructor.

It can be done with adding com.fasterxml.jackson.module.paramnames.ParameterNamesModule into ObjectMapper:

new ObjectMapper()
		.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))

(Btw: this module is registered by default in SpringBoot. If you use ObjectMapper bean from JacksonObjectMapperConfiguration or if you create your own ObjectMapper with bean Jackson2ObjectMapperBuilder then you can skip manual registration of the module)

For example:

public class FieldValidationError {
  private final String field;
  private final String error;

  @JsonCreator
  public FieldValidationError(String field,
  							  String error) {
	this.field = field;
	this.error = error;
  }

  public String getField() {
  	return field;
  }

  public String getError() {
	return error;
  }
}

and ObjectMapper deserializes this json without any errors:

{
  "field": "email",
  "error": "some text"
}

Solution 4 - Java

It's 2021, I had the same issue. Unfortunately, the previous answers in this thread weren't helpful in my case, because:

  1. I need to deserialize java.net.HttpCookie class object. I can't modify it.
  2. Due to some reasons, I use jackson-databind-2.11.0 version. ParameterNamesModule has been added in 3+ version.

So, here's the solution I've found for my case. You can just use DeserializationProblemHandler:

    objectMapper.addHandler(new DeserializationProblemHandler() {


        @Override
        public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, ValueInstantiator valueInsta, JsonParser p, String msg) throws IOException {
            return super.handleMissingInstantiator(ctxt, instClass, valueInsta, p, msg);
        }

    });

Just return the object which you're expecting

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
QuestionMichal KrasnyView Question on Stackoverflow
Solution 1 - JavaSergeiView Answer on Stackoverflow
Solution 2 - JavatkruseView Answer on Stackoverflow
Solution 3 - JavaVladyslav DiachenkoView Answer on Stackoverflow
Solution 4 - JavaA. KashnikovView Answer on Stackoverflow