Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create default constructor for immutable class

I like to make my objects immutable based on this article (Why objects must be immutable).

However, I am trying to parse an object using Jackson Object Mapper. I was initially getting JsonMappingException: No suitable constructor found for type [simple type, class ]: cannot instantiate from JSON object.

I could fix it as mentioned here, by providing a default constructor and making my fields non-final.

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

@AllArgsConstructor
// @NoArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Data
public class School {

    @NonNull
    private final String schoolId;

    @NonNull
    private final String schoolName;
}

What is a good programming style that I should follow to overcome this problem? Is the only way around is to make my objects mutable?

Can I use a different mapper that does not use the default constructor?

like image 420
SyncMaster Avatar asked Sep 21 '18 05:09

SyncMaster


People also ask

Does immutable class have private constructor?

I have seen private constructor,factory method in Singleton pattern which makes sense. But when we talk of object immutability, are we also restricting object construction/instantiation when we mention private constructor and static factory methods?? Yes.

Can immutable class have setter?

Now, it is clear that Person is not an Immutable class. This doesn't mean, an instance of Person can't be a member of another class that is (supposedly) immutable. Now, we have an immutable class, but, it does contain a setter. This setter actively changes a member of the (final) field p.

Can we extend immutable class in Java?

To create an immutable class in Java, you have to do the following steps. Declare the class as final so it can't be extended. Make all fields private so that direct access is not allowed. Don't provide setter methods for variables.

What is an immutable object and how do you construct it?

An object is immutable if its state cannot change after construction. Immutable objects don't expose any way for other objects to modify their state; the object's fields are initialized only once inside the constructor and never change again.


2 Answers

You can use a Jackson factory (method annotated with @JsonCreator) that reads fields off a map and calls your non-default constructor:

class School {
    //fields

    public School(String id, String name) {
        this.schoolId = id;
        this.schoolName = name;
    }

    @JsonCreator
    public static School create(Map<String, Object> object) {
        return new School((String) object.get("schoolId"), 
                          (String) object.get("schoolName"));
    }

    //getters
}

Jackson will call the create method with a Map version of the json. And this effectively solves the problem.

I believe your question looks for a Jackson solution, rather than a new pattern/style.

like image 110
ernest_k Avatar answered Sep 21 '22 20:09

ernest_k


TL;DR: using lombok and avoiding a default constructor

  • make immutable data class using @Value
  • annotate all your fields with @JsonProperty("name-of-property")
  • add lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty to your lombok.config to copy those to generated constructors
  • create an all-args constructor annotated with @JsonCreator

example:

@Value
@AllArgsConstructor(onConstructor_ = @JsonCreator)
class School {
    @JsonProperty("schoolId")
    String schoolId;
    @JsonProperty("schoolName")
    String schoolName;
}

long answer

There is an imo better alternative to a static factory method annotated with @JsonCreator, and that is having a constructor for all Elements (as is required for immutable classes anyway). Annotate that with @JsonCreator and also annotate all parameters with @JsonProperty like this:

class School {
    //fields

    @JsonCreator
    public School(
            @JsonProperty("id") String id,
            @JsonProperty("name") String name) {
        this.schoolId = id;
        this.schoolName = name;
    }

    //getters
}

Those are the options the @JsonCreator annotation gives you. It describes them like this in its documentation:

  • Single-argument constructor/factory method without JsonProperty annotation for the argument: if so, this is so-called "delegate creator", in which case Jackson first binds JSON into type of the argument, and then calls creator. This is often used in conjunction with JsonValue (used for serialization).
  • Constructor/factory method where every argument is annotated with either JsonProperty or JacksonInject, to indicate name of property to bind to

You might not even need to explicitly specify the parameter name under some circumstances. The documentation regarding that for @JsonCreator further states:

Also note that all JsonProperty annotations must specify actual name (NOT empty String for "default") unless you use one of extension modules that can detect parameter name; this because default JDK versions before 8 have not been able to store and/or retrieve parameter names from bytecode. But with JDK 8 (or using helper libraries such as Paranamer, or other JVM languages like Scala or Kotlin), specifying name is optional.

Alternatively this will also work nicely with lombok version 1.18.3 or up, where you can add lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty to your lombok.config and therefore have it copy the JsonProperty annotations to the constructor, given that you do annotate all fields with it (which one should do anyway imo). To put the @JsonCreator-annotation on the constructor, you can use the experimental onX feature. Using lombok's @Value for immutable data classes, your DTO then might just look like this (untested):

@Value
//@AllArgsConstructor(onConstructor = @__(@JsonCreator)) // JDK7 or below
@AllArgsConstructor(onConstructor_ = @JsonCreator) // starting from JDK8
class School {
    @JsonProperty("schoolId")
    String schoolId;
    @JsonProperty("schoolName")
    String schoolName;
}
like image 40
Felk Avatar answered Sep 18 '22 20:09

Felk