12

I have a controller that accepts ObjectNode as @RequestBody.

That ObjectNode represents json with some user data

{
    "given_name":"ana",
    "family_name": "fabry",
    "email": "fabry@gmail.com",
    "password": "mypass",
    "gender": "FEMALE"
}

Controller.java

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public JsonNode createUser(@RequestBody ObjectNode user){
        return userService.addUser(user);
 }

I want to get user as ObjectNode convert it to Java POJO save it to database and again return it as JsonNode.

UserServiceImpl.java

    private final UserRepository userRepository;
    private final UserMapper userMapper;

    @Override
    public JsonNode addUser(@RequestBody ObjectNode user) {
        try {
            return userMapper.fromJson(user)
                    .map(r -> {
                        final User created = userRepository.save(r);
                        return created;
                    })
                    .map(userMapper::toJson)
                    .orElseThrow(() -> new ResourceNotFoundException("Unable to find user"));
        } catch (RuntimeException re) {
            throw re;
        }
    }

To convert ObjectNode to POJO

I did this in my UserMapper class:

public Optional<User> fromJson(ObjectNode jsonUser) {
  User user = objectMapper.treeToValue(jsonUser, User.class);
}

Also, to write object to JsonNode I did this:

public JsonNode toJson(User user) {
        ObjectNode node = objectMapper.createObjectNode();
        node.put("email", user.email);
        node.put("password", user.password);
        node.put("firstName", user.firstName);
        node.put("lastName", user.firstName);
        node.put("gender", user.gender.value);
        node.put("registrationTime", user.registrationTime.toString());
        return node;
}

User.java

@Document(collection = "user")
@Builder
@AllArgsConstructor
public class User {

    @Indexed(unique = true)
    public final String email;
    @JsonProperty("password")
    public final String password;
    @JsonProperty("firstName")
    public final String firstName;
    @JsonProperty("lastName")
    public final String lastName;
    @JsonProperty("gender")
    public final Gender gender;
    @JsonProperty("registrationTime")
    public final Instant registrationTime;

    public static User createUser(
            String email,
            String password,
            String firstName,
            String lastName,
            Gender gender,
            Instant registrationTime){
        return new User(email, password, firstName, lastName, gender, registrationTime);
    }
}

When I run my application, this is the error I am receiving:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.domain.User` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

I have read about the error, and it seems this error occurs because Jackson library doesn't know how to create a model which doesn't have an empty constructor and the model contains a constructor with parameters which I annotated its parameters with @JsonProperty("fieldName"). But even after applying @JsonProperty("fieldName") I am still getting the same error.

I have defined ObjecatMapper as Bean

    @Bean
    ObjectMapper getObjectMapper(){
        return new ObjectMapper();
    }

What am I missing here?

user9347049
  • 1,927
  • 3
  • 27
  • 66

3 Answers3

6

I could reproduce the exception. Then I added an all-args constructor with each parameter annotated with the right @JsonProperty.

@JsonCreator
public User( 
    @JsonProperty("email") String email,
    @JsonProperty("password") String password,
    @JsonProperty("firstName") String firstName,
    @JsonProperty("lastName") String lastName,
    @JsonProperty("gender") String gender,
    @JsonProperty("registrationTime") Instant registrationTime){
            super();
            this.email = email;
            this.password = password;
            this.firstName = firstName;
            this.lastName = lastName;
            this.gender = gender;
            this.registrationTime = registrationTime;
}

Now, it creates the instance, but I get other mapping errors (Unrecognized field "given_name") which you should be able to resolve.

Sree Kumar
  • 2,012
  • 12
  • 9
  • Okay, thank you. Yes, I did like you now. But I am getting null pointer exception from method `toJson` on this line of code `node.put("registrationTime", user.registrationTime.toString());` do you maybe know how can I handle it? – user9347049 Nov 28 '21 at 12:57
  • That may be because the input doesn't have `registrationTime` in it, so `null` is being to the field. You may give it a default value in the constructor above, if `null` is passed to it. – Sree Kumar Nov 28 '21 at 13:03
  • So maybe the best is to put it as Optional. – user9347049 Nov 28 '21 at 13:05
2

Register Jackson ParameterNamesModule, which will automatically map JSON attributes to the corresponding constructor attributes and therefore will allow you to use immutable classes.

Adam Siemion
  • 15,569
  • 7
  • 58
  • 92
1

The error happened in scala due to missing the name of keys in the constructor.

I missed specifying the names (example currency) in the snippet below - @JsonProperty("currency")

For converting json to case-class using scala and jackson, we can do it in the following way -

Add the jackson dependency in maven/sbt. If you are using sbt, then add the below entry in build.sbt

libraryDependencies += "com.fasterxml.jackson.core" % "jackson-core" % "2.12.5"

Define the case class -

import com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty}

import java.util.Currency

@JsonCreator
case class TopUp (
                   @JsonProperty("cardId") cardId : Long,
                   @JsonProperty("amount") amount : BigDecimal,
                   @JsonProperty("currency") currency: Currency
                 )
object TopUp  {


}

In the main method -

import com.fasterxml.jackson.databind.ObjectMapper
import models.oyster.mappers.request.TopUp

object App {
  def main(args : Array[String]): Unit ={
    val tmp =
      """
        |{
        |    "cardId": 718908976540,
        |    "amount": 30.00,
        |    "currency": "GBP"
        |
        |}
        |""".stripMargin

    val objectMapper = new ObjectMapper()
    val inputParsed = objectMapper.readValue(tmp, classOf[TopUp])
    println("parsed input :: " + inputParsed)

  }

}
ForeverLearner
  • 1,901
  • 2
  • 28
  • 51