Friday 15 September 2023

no String-argument constructor/factory method to deserialize from String value

 The Cause

Before diving deep into the exception, let’s understand what its stack trace means.

Typically, “no String-argument constructor/factory method to deserialize from String” tells us that Jackson fails to find a suitable constructor or factory method to deserialize a JSON string into an object.

In short, the exception occurs when something goes wrong while deserializing a JSON string.

The leading cause behind it is trying to perform deserialization using double quotes ”…” instead of bracelets {…}.

Jackson interprets the double quotes as a string. So it expects the target object to have a string argument constructor or a factory method. Hence the error no String-argument constructor/factory method.

Another reason would be accidentally using the convertValue() method instead of readValue().

Reproducing no String-argument constructor/factory method to deserialize from String

Now that we know what the stack trace means, let’s see how to reproduce it in practice.

For instance, let’s consider the Person class:

    
        public class Person {

            private String firstName;
            private String lastName;
            private Email email;

            public String getFirstName() {
                return firstName;
            }
            public void setFirstName(String firstName) {
                this.firstName = firstName;
            }
            public String getLastName() {
                return lastName;
            }
            public Email getEmail() {
                return email;
            }
            public void setEmail(Email email) {
                this.email = email;
            }
        }
    

Next, we are going to create the Email class:

    
        public class Email {
            private String account;
            public String getAccount() {
                return account;
            }
            public void setAccount(String account) {
                this.account = account;
            }
        }
    

Now, let’s add a static method to deserialize a JSON string into a Person object:

    
        public static void deserializeJsonString() {
            ObjectMapper mapper = new ObjectMapper();
            String personJson = "{" + 
                                    "\"firstName\": \"Azhrioun\"," + 
                                    "\"lastName\": \"Abderrahim\"," + 
                                    "\"email\": \"gaccount@gmail.com\""  +  
                                "}";

            try {
                Person person = mapper.readValue(personJson, Person.class);
                System.out.println("Email Account: " + person.getEmail().getAccount());
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
    

As we can see, we used a string value to denote the JSON property email.

Finally, let’s execute the method and take a close look at the stack trace:

    
        com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of ...
        no String-argument constructor/factory method to deserialize from String value ('gaccount@gmail.com')
    

As shown above, Jackson fails with the exception because it could not convert the specified JSON value gaccount@gmail.com, which is a string, into an object of the Email type.

This behavior makes sense. In order to perform deserialization, Jackson needs to call a constructor which takes String email as an argument.

Fixing the Exception

The easiest solution would be defining a string argument constructor in our class Person.

This constructor will be used by Jackson later to handle the JSON deserialization.

Here’s an example:

    
        public Person(@JsonProperty("firstName") String firstName, 
                      @JsonProperty("lastName") String lastName, 
                      @JsonProperty("email") String email) {
            this.firstName = firstName;
            this.lastName = lastName;
        
            Email emailObj = new Email();
            emailObj.setAccount(email);
            this.email = emailObj;
        }
    

We used the @JsonProperty annotation to denote the property names in the given JSON.

As we can see, we created a new Email object. Then, we used the passed string argument to set the account field.

Alternatively, we can annotate a custom method that accepts a string parameter with @JsonProperty(“email”).

So, let’s see it in action:

    
        @JsonProperty("email")
        public void deserializeEmail(String email) {
            Email emailObj = new Email();
            emailObj.setAccount(email);
            this.email = emailObj;
        }
    

That way, we tell Jackson to call deserializeEmail() when deserializing the JSON property “email”: “gaccount@gmail.com”.

Another approach would be using a static factory method instead of the string argument constructor:

    
        @JsonCreator
        public static Person of(@JsonProperty("firstName") String firstName, 
                                @JsonProperty("lastName") String lastName, 
                                @JsonProperty("email") String email) {
            Person person = new Person();
            person.firstName = firstName;
            person.lastName = lastName;

            Email emailObj = new Email();
            emailObj.setAccount(email);
            person.email = emailObj;

            return person;
        }
    

Simply put, @JsonCreator marks the factory method defining how to deserialize the JSON. It’s a good practice when the specified JSON doesn’t match the target class.

No comments:

Post a Comment