0

A quick note: Yes, I read this. Unfortunately, t doesn't answer all the questions I have

How does Jackson deserialization work by default¹? What I so far found out is that Jackson looks for a no-args and then sets the fields with setters (or, if setters are absent, reflexion). I have a few questions in mind.

  1. Does a no-args have to be public?
// consider this
public class Person {
    private int id;
    private String name;
    private Person() {}
}
  1. Does a no-args have to be the only constructor available? What if there are an explicit no-args and an all-args, for example? What happens then?
// consider this
public class Person {
    private int id;
    private String name;
    public Person() {}
    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
  1. Can Jackson figure out what to do if there's no no-args? Will it take an all-args?
// consider this
public class Person {
    private int id;
    private String name;
    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
  1. Would Jackson assemble an instance if some properties are not set by a constructor (assuming it would take a "some-args" constructor)? Would Jackson invoke available setters, or would it resort to reflexion?
// consider this
public class Person {
    private int id;
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public void setId(int id) {
        this.id = id;
    }
}

¹ That is, with no extra effort on behalf of a developer: annotations, custom deserializers, etc.

  • What happens when you try these tests for yourself? What do you observe? Do your observations effectively answer the question, or do they lead to new (potentially more specific) questions? – andrewJames Jun 02 '23 at 19:58
  • 1
    It'd probably be beneficial for you to try this by yourself on an online IDE like https://www.jdoodle.com/ – Ale Zalazar Jun 02 '23 at 22:29
  • @andrewJames such basic testing would not be so easy on my slow laptop currently trying not to faint while digesting a Docker-run app. Besides, others may find this information useful too. SO is a "knowledge database", after all, isn't it? – Sergey Zolotarev Jun 03 '23 at 01:50
  • @AleZalazar how do I know what Jackson does behind the scenes to get the result? Online IDEs don't have a debugging utility, do they? – Sergey Zolotarev Jun 03 '23 at 07:38
  • @AleZalazar I don't know how to get it work. How do you import in jdoodle? These imports, `import com.fasterxml.jackson.core.jackson.databind.ObjectMapper; import lib.Person;` don't work, jdoodle can't find the files – Sergey Zolotarev Jun 03 '23 at 15:20
  • "_I don't know how to get it to work._" - Use the [advanced IDE](https://www.jdoodle.com/online-java-compiler-ide/). This allows you to specify 3rd party libraries (for example, as a Maven lib using `com.fasterxml.jackson.core:jackson-databind:2.15.0-rc3`). And your `Person` can be uploaded, or a nested class. @Sergey – andrewJames Jun 03 '23 at 15:55
  • @andrewJames I did use the advanced IDE. Please see the screenshot: https://ibb.co/KW08SgZ – Sergey Zolotarev Jun 03 '23 at 16:16
  • Step 1: Fix your import for `ObjectMapper`. Use `import com.fasterxml.jackson.databind.ObjectMapper;` - compare that to what's in your screenshot. (This is getting a bit too far off-topic, I think.) – andrewJames Jun 03 '23 at 16:22
  • @andrewJames no, it doesn't work too (nor should it, you didn't include the entire group id, `com.fasterxml.jackson.core`) – Sergey Zolotarev Jun 03 '23 at 17:12
  • Take a [closer look](https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/ObjectMapper.html), please. Are you mixing up the library (JAR file) name with the fully qualified class name? But like I say, we have now strayed a long way from your question. – andrewJames Jun 03 '23 at 17:25
  • @andrewJames Even if I use your import, it doesn't work. The console says, "Maven library download failed". What's your library declaration? I tried both `com.fasterxml.jackson.core:jackson.databind:2.15.2` and `com.fasterxml.jackson.core:jackson.databind:jar:2.15.2` – Sergey Zolotarev Jun 03 '23 at 18:01
  • I gave you an example of a valid Maven library declaration in an earlier comment. You even showed me another valid example in a screenshot you provided. Use one of those. And then fix your Java `import` statement. I cannot help you further. – andrewJames Jun 03 '23 at 18:43

1 Answers1

1

Note. You can run all the snippets on jdoodle. Make sure you include the Jackson dependency in the maven-lib folder. Press ...Manage libs, then paste com.fasterxml.jackson.core:jackson-databind:2.15.2 and finally press Add library. Notice, the Person class includes a package declaration. It's important, don't forget to add it too

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import lib.Person;

public class MyClass {
   public static void main(String args[]) throws JsonProcessingException {
      ObjectMapper mapper = new ObjectMapper();
      String JSON = "{\"id\": 1, \"name\": \"Sergey\"}";
      Person me = mapper.readValue(JSON, Person.class);
       System.out.println(me);
    }
}
  1. Do I need to include setters?

Yes, you do (assuming we discuss the default scenario as defined in your question). Watch out! The exception you're going to get in case you don't include setters is not going to be very informative

package lib;

public class Person {
    private int id;
    private String name;
    
    public Person() {
        System.out.println("Person's public no-args is called...");
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Person's public no-args is called...

Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "id" (class lib.Person), not marked as ignorable (0 known properties: ])
 at [Source: (String)"{"id": 1, "name": "Sergey"}"; line: 1, column: 9] (through reference chain: lib.Person["id"])
...
  1. Does a no-args have to be public?

No, it doesn't. You can make the setters private too

package lib;

public class Person {
    private int id;
    private String name;
    
    private Person() {
        System.out.println("Person's private no-args is called...");
    }
    
    public void setId(int id) {
        System.out.println("setId() is called...");
        this.id = id;
    }
    
    public void setName(String name) {
        System.out.println("setName() is called...");
        this.name = name;
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Person's private no-args is called...
setId() is called...
setName() is called...
Person[id=1, name=Sergey]
  1. Does a no-args have to be the only constructor available? What if there are an explicit no-args and an all-args, for example? What happens then?

Jackson wouldn't care about your other constructor. You can even make your no-args private thus nudging Jackson to call your public canonical – it couldn't care less

package lib;

public class Person {
    private int id;
    private String name;
    
    private Person() {
        System.out.println("Person's private no-args is called...");
    }
    
    public Person(int id, String name) {
        System.out.println("Person's public canonical is called...");
        this.id = id;
        this.name = name;
    }
    
    public void setId(int id) {
        System.out.println("setId() is called...");
        this.id = id;
    }
    
    public void setName(String name) {
        System.out.println("setName() is called...");
        this.name = name;
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Person's private no-args is called...
setId() is called...
setName() is called...
Person[id=1, name=Sergey]
  1. Can Jackson figure out what to do if there's no no-args? Will it take an all-args?

No, it won't

package lib;

public class Person {
    private int id;
    private String name;
    
    public Person(int id, String name) {
        System.out.println("Person's public canonical is called...");
        this.id = id;
        this.name = name;
    }
    
    public void setId(int id) {
        System.out.println("setId() is called...");
        this.id = id;
    }
    
    public void setName(String name) {
        System.out.println("setName() is called...");
        this.name = name;
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `lib.Person` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"id": 1, "name": "Sergey"}"; line: 1, column: 2]

I'm going off on a tangent a little bit since the question is explicitly about Jackson's default behavior, but if you annotate your canonical constructor as @JsonCreator and manually map your constructor arguments with @JsonProperty, Jackson will finally condescend to use it

package lib;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    private int id;
    private String name;
    
    @JsonCreator
    public Person(@JsonProperty("id") int id, @JsonProperty("name") String name) {
        System.out.println("Person's public canonical is called...");
        this.id = id;
        this.name = name;
    }
    
    public void setId(int id) {
        System.out.println("setId() is called...");
        this.id = id;
    }
    
    public void setName(String name) {
        System.out.println("setName() is called...");
        this.name = name;
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Person's public canonical is called...
Person[id=1, name=Sergey]

If you omit @JsonProperty annotations assuming Jackson will map the properties automatically (after all, the names and types match), you're going to have problems. Again, the error message is not going to help you at all

package lib;

import com.fasterxml.jackson.annotation.JsonCreator;

public class Person {
    private int id;
    private String name;
    
    @JsonCreator
    public Person(int id, String name) {
        System.out.println("Person's public canonical is called...");
        this.id = id;
        this.name = name;
    }
    
    public void setId(int id) {
        System.out.println("setId() is called...");
        this.id = id;
    }
    
    public void setName(String name) {
        System.out.println("setName() is called...");
        this.name = name;
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `lib.Person`: Argument #0 of constructor [constructor for `lib.Person` (2 args), annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)} has no property name (and is not Injectable): can not use as property-based Creator
 at [Source: (String)"{"id": 1, "name": "Sergey"}"; line: 1, column: 1]

As a way to avoid manual mapping with @Jsonou can register the ParameterNamesModule. It will allow Jackson to perform the mapping automatically if JSON properties' names match those of class fields. Jackson 3.x supports ParameterNamesModule by default

  ObjectMapper mapper = new ObjectMapper()
                            .registerModule(new ParameterNamesModule());
  1. Would Jackson assemble an instance if some properties are not set by a constructor (assuming it would take a "some-args" constructor)? Would Jackson invoke available setters, or would it resort to reflexion?

As stated above, in a default scenario, Jackson is going to be happy with a no-args only. However, if you use the annotations mentioned in the previous item, Jackson will do exactly that: invoke a "some-args" constructor and then call setters to initialize the remaining fields

package lib;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    private int id;
    private String name;
    
    @JsonCreator
    public Person(@JsonProperty("name") String name) {
        System.out.println("Person's public \"some-args\" is called...");
        this.name = name;
    }
    
    public void setId(int id) {
        System.out.println("setId() is called...");
        this.id = id;
    }
    
    public void setName(String name) {
        System.out.println("setName() is called...");
        this.name = name;
    }

    @Override
    public String toString() {
       return String.format("Person[id=%d, name=%s]", id, name);
    }
}
Person's public "some-args" is called...
setId() is called...
Person[id=1, name=Sergey]