16

In my java spring application, I am working with hibernate and jpa, and i use jackson to populate data in DB.

Here is the User class:

@Data
@Entity
public class User{

    @Id
    @GeneratedValue
    Long id;

    String username;
    String password;
    boolean activated;

    public User(){}
}

and the second class is:

@Entity
@Data
public class Roles {

    @Id
    @GeneratedValue
    Long id;

    @OneToOne
    User user;

    String role;

    public Roles(){}


}

In the class Roles i have a property of User and then i made a json file to store the data:

[ {"_class" : "com.example.domains.User", "id": 1, "username": "Admin", "password": "123Admin123","activated":true}
,
  {"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}]

Unfortunately, when i run the app it complains with:

.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.example.domains.User: no int/Int-argument constructor/factory method to deserialize from Number value (1)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.example.domains.Roles["user"])

The problem comes from

{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}

and when i remove the above line the app works well.

I think, it complains because it cannot make an instance of user. So, how can i fix it?

Bengi Besçeli
  • 3,638
  • 12
  • 53
  • 87
Jeff
  • 7,767
  • 28
  • 85
  • 138
  • 1
    It says right there what the problem is: it cant map the integer 1 onto a field that has the type "User". You should either have the user object instead of a foreign key reference in the input. OR do what i prefer, write a DTO containing the fields you are really interested in, then apply whatever business logic you need to get an entity working. – Robin Jonsson Feb 01 '17 at 21:23
  • i didn't get u. could u please write an answer? – Jeff Feb 01 '17 at 21:26
  • From a business perspective, is that one to one correct? – cassiomolin Feb 09 '17 at 10:45
  • I guess, the problem is coming from the fact that you are setting the value of "id" whereas it should be autogenerated. – UberHans Feb 09 '17 at 14:54
  • http://stackoverflow.com/questions/5489532/jackson-json-library-how-to-instantiate-a-class-that-contains-abstract-fields – Fortran Feb 12 '17 at 17:00

7 Answers7

12

Do yourself a favor and stop using your Entities as DTOs!

JPA entities have bidirectional relations, JSON objects don't, I also believe that the responsibilities of an Entity is very different from a DTO, and although joining these responsibilities into a single Java class is possible, in my experience it is a very bad idea.

Here are a couple of reasons

  • You almost always need more flexibility in the DTO layer, because it is often related to a UI.
  • You should avoid exposing primary keys from your database to the outside, including your own UI. We always generate an additional uniqueId (UUID) for every publicly exposed Entity, the primary key stays in the DB and is only used for joins.
  • You often need multiple views of the same Entity. Or a single view of multiple entities.
  • If you need to add a new entity to a relation with an existing, you will need find the existing one in the database, so posting the new and old object as a single JSON structure has no advantage. You just need the uniqueId of the existing, and then new.

A lot of the problems developers have with JPA, specifically with regards to merging comes from the fact that they receive a detached entity after their json has been deserialized. But this entity typically doesn't have the OneToMany relations (and if it does, it's the parent which has a relation to the child in JSON, but in JPA it is the child's reference to the parent which constitutes the relationship). In most cases you will always need to load the existing version of the entity from the database, and then copy the changes from your DTO into the entity.

I have worked extensively with JPA since 2009, and I know most corner cases of detachment and merging, and have no problem using an Entity as a DTO, but I have seen the confusion and types of errors that occur when you hand such code over to some one who is not intimately familiar with JPA. The few lines you need for a DTO (especially since you already use Lombok), are so simple and allows you much more flexibility, than trying to save a few files and breaking the separation of concerns.

Klaus Groenbaek
  • 4,820
  • 2
  • 15
  • 30
10

Jackson provide ObjectIdResolver interface for resolving the objects from ids during de-serialization.

In your case you want to resolve the id based from the JPA/hibernate. So you need to implement a custom resolver to resolve id by calling the JPA/hierbate entity manager.

At high level below are the steps:

  1. Implement a custom ObjectIdResolver say JPAEntityResolver (you may extends from SimpleObjectIdResolver). During resolving object it will call JPA entity manager class to find entity by given id and scope(see. ObjectIdResolver#resolveId java docs)

    //Example only;
    @Component
    @Scope("prototype") // must not be a singleton component as it has state
    public class JPAEntityResolver extends SimpleObjectIdResolver {
    //This would be JPA based object repository or you can EntityManager instance directly.
    private PersistentObjectRepository objectRepository;
    
    @Autowired
    public JPAEntityResolver (PersistentObjectRepository objectRepository) {
        this.objectRepository = objectRepository;
    }
    
    @Override
    public void bindItem(IdKey id, Object pojo) {
        super.bindItem(id, pojo);
    }
    
    @Override
    public Object resolveId(IdKey id) {
        Object resolved = super.resolveId(id);
        if (resolved == null) {
            resolved = _tryToLoadFromSource(id);
            bindItem(id, resolved);
        }
    
        return resolved;
    }
    
    private Object _tryToLoadFromSource(IdKey idKey) {
        requireNonNull(idKey.scope, "global scope does not supported");
    
        String id = (String) idKey.key;
        Class<?> poType = idKey.scope;
    
        return objectRepository.getById(id, poType);
    }
    
    @Override
    public ObjectIdResolver newForDeserialization(Object context) {
        return new JPAEntityResolver(objectRepository);
    }
    
    @Override
    public boolean canUseFor(ObjectIdResolver resolverType) {
        return resolverType.getClass() == JPAEntityResolver.class;
    }
    

    }

  2. Tell Jackson to use a custom id resolver for a class, by using annotation JsonIdentityInfo(resolver = JPAEntityResolver.class). For e.g.

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
                  property = "id",
                  scope = User.class,
                  resolver = JPAObjectIdResolver.class)
    public class User { ... }
    
  3. JPAObjectIdResolver is a custom implementation and will have dependency on other resources( JPA Entity Manager) which might not be known to Jackson. So Jackson need help to instantiate resolver object. For this purpose, you need to supply a custom HandlerInstantiator to ObjectMapper instance. (In my case I was using spring so I asked spring to create instance of JPAObjectIdResolver by using autowiring)

  4. Now de-serialization should work as expected.

Hope this helps.

skadya
  • 4,330
  • 19
  • 27
  • This is quite involved but does work. I found that step 3 needs https://stackoverflow.com/a/44070717/204295 steps 2 & 3 to implement and you also need to annotate the parent object relationship to the child with a @JsonProperty with the key that you're supplying from the web form. – Ben Ketteridge Jan 03 '20 at 15:24
1

I have changed the json file to :

[
  {"_class" : "com.example.domains.User",
   "id": 1,
   "username": "Admin",
   "password": "123Admin123",
   "activated":true
  },
  {
    "_class" : "com.example.domains.Roles",
    "id": 1,
    "user":{"_class" : "com.example.domains.User",
            "id": 1,
            "username": "Admin",
            "password": "123Admin123",
            "activated":true
          },
    "role": "Admin"
  }
]

But i still think, the best ways is using a foreign key to user record. Any solution is welcomed

Jeff
  • 7,767
  • 28
  • 85
  • 138
1

If your bean doesn't strictly adhere to the JavaBeans format, Jackson has difficulties.

It's best to create an explicit @JsonCreator constructor for your JSON model bean, e.g.

class User {
    ...

   @JsonCreator
   public User(@JsonProperty("name") String name, 
               @JsonProperty("age") int age) {
            this.name = name;
            this.age = age;
   }

   ..
}
Barry O'Neill
  • 435
  • 6
  • 16
1

1-1 mapping of fields works well , but when it comes to complex object mapping , better to use some API. You can use Dozer Mapping or Mapstruct to map Object instances. Dozer has spring integration also.

Ankur Srivastava
  • 855
  • 9
  • 10
1

You could specify non default constructors and then use a custom deserialiser.

Something like this should work (it has not been tested).

public class RolesDeserializer extends StdDeserializer<Roles> { 

    public RolesDeserializer() { 
        this(null); 
    } 

    public RolesDeserializer(Class<?> c) { 
        super(c); 
    }

    @Override
    public Roles deserialize(JsonParser jp, DeserializationContext dsctxt) 
      throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        long id = ((LongNode) node.get("id")).longValue();
        String roleName = node.get("role").asText();

        long userId = ((LongNode) node.get("user")).longValue();

        //Based on the userId you need to search the user and build the user object properly
        User user = new User(userId, ....);

        return new Roles(id, roleName, user);
    }
}

Then you need to register your new deserialiser (1) or use the @JsonDeserialize annotation (2)

(1)

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Item.class, new RolesDeserializer());
mapper.registerModule(module);

Roles deserializedRol = mapper.readValue(yourjson, Roles.class);

(2)

@JsonDeserialize(using = RolesDeserializer.class)
@Entity
@Data
public class Roles {
    ...
}

Roles deserializedRol = new ObjectMapper().readValue(yourjson, Roles.class);
alfcope
  • 2,327
  • 2
  • 13
  • 21
0
public class Roles {

    @Id
    @GeneratedValue
    Long id;

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    @JsonIdentityReference(alwaysAsId = true)
    @OneToOne
    User user;

    String role;

    public Roles(){}

}
Robin Dijkhof
  • 18,665
  • 11
  • 65
  • 116