Quentin Lagadec
Quentin Lagadec

Reputation: 231

Deserialize JSON with spring : Unresolved forward references Jackson Exception

I work on API Rest project with Spring. I have a service "CreateMateriel" which takes as parameter data JSON :

JSON of Materiel Object

{
  "agence": 1, 
  "code": "001",
  "type": "MyType"
}

"Materiel" has a relation Many to One with "Agence". I put a @JsonIdentityInfo tag to use Agence's Id and not Agence's Object (After seeing this topic)

@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "idAgence")
@JsonIdentityReference(alwaysAsId = true)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agence")
private Agence agence;

But when I send JSON on POST /materiels I have this exception:

2017-05-16 18:00:53.021  WARN 8080 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Unresolved forward references for: 
 at [Source: java.io.PushbackInputStream@767dfd7a; line: 5, column: 1]Object id [1] (for fr.app.domain.Agence) at [Source: java.io.PushbackInputStream@767dfd7a; line: 2, column: 14].; nested exception is com.fasterxml.jackson.databind.deser.UnresolvedForwardReference: Unresolved forward references for: 
 at [Source: java.io.PushbackInputStream@767dfd7a; line: 5, column: 1]Object id [1] (for fr.app.domain.Agence) at [Source: java.io.PushbackInputStream@767dfd7a; line: 2, column: 14].

After many research I have seen the use of ObjectIdResolver in JsonIdentityInfo ... But I think this is not the best solution. This is why I ask for your help in detecting the source of the problem. Thank's

MaterielController.java

package fr.app.controllers;

import fr.app.domain.Materiel;
import fr.app.services.MaterielService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Collection;

@RestController @RequestMapping(value = "/materiels")
public class MaterielController {
    @Resource
    private MaterielService materielService;

    @RequestMapping(method = RequestMethod.POST)
    public Materiel createMateriel(@RequestBody Materiel materiel) {
        return this.materielService.createMateriel(materiel);
    }

    @RequestMapping(method = RequestMethod.GET)
    public Collection<Materiel> getAllMateriels() {
        return this.materielService.getAllMateriels();
    }

    @RequestMapping(value = "/{code}", method = RequestMethod.GET)
    public Materiel getMaterielByCode(@PathVariable(value = "code") String code) {
        //find materiel by code
        return this.materielService.getMaterielByCode(code);

    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void deleteMateriel(@PathVariable(value = "id") Long id) {
        this.materielService.deleteMateriel(id);

    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public Materiel updateMateriel(@PathVariable(value = "id") Long id, @RequestBody
            Materiel materiel) {

            materiel.setIdMateriel(id);

        return this.materielService.updateMateriel(materiel);

    }
}

Materiel.java

package fr.app.domain;

import com.fasterxml.jackson.annotation.*;

import javax.persistence.*;

@Entity
public class Materiel {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idMateriel;

    @Column(name = "type_materiel", nullable = false)
    private String type;

    @Column(name = "code_materiel", unique = true, nullable = false)
    private String code;

    @JsonIdentityInfo(
            generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "idAgence")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "agence")
    private Agence agence;

    public Materiel() {    }

    public Materiel(String type, String code, String dateScan) {
        this.type = type;
        this.code = code;
        this.dateScan = dateScan;
    }

    public Long getIdMateriel() {
        return idMateriel;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public Agence getAgence() {
        return agence;
    }

    public void setAgence(Agence agence) {
        if(this.agence != null)
            this.agence.deleteMateriel(this);
        this.agence = agence;
        this.agence.addMateriel(this);
    }
}

MaterielService.java

package fr.app.services;

import fr.app.domain.Materiel;
import fr.app.repositories.MaterielRepository;
import org.apache.commons.collections.IteratorUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collection;

@Service(value = "materielService")
public class MaterielServiceImpl implements MaterielService {
    @Resource
    private MaterielRepository materielRepository;

    ...

    @Override
    public Materiel createMateriel(Materiel materiel) {
        return this.materielRepository.save(materiel);
    }

   ...
}

Agence.java

package fr.app.domain;

import com.fasterxml.jackson.annotation.*;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
public class Agence {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idAgence;

    @Column(name = "nom",unique = true, nullable = false)
    private String nom;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "agence")
    private Set<Materiel> materiels = new HashSet<Materiel>();

    public Agence() { }

    public Agence(String nom) {
        this.nom = nom;
    }

    public Long getIdAgence() {
        return idAgence;
    }

    public String getNom() {
        return nom;
    }

    public void setNom(String nom) {
        this.nom = nom;
    }

    public Set<Materiel> getMateriels() {
        return materiels;
    }

    public void setMateriels(Set<Materiel> materiels) {
        this.materiels = materiels;
    }

    public void addMateriel(Materiel materiel) {
        this.materiels.add(materiel);
    }

    public void deleteMateriel(Materiel materiel) {
        this.materiels.remove(materiel);
    }

}

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.0</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.2.9.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>5.2.9.Final</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.2.2</version>
        <scope>compile</scope>
    </dependency>

    <!-- Needed for JSON View -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.8.6</version>
    </dependency>
</dependencies>

Upvotes: 5

Views: 13874

Answers (4)

JavaDevSweden
JavaDevSweden

Reputation: 2284

You seem to have a cyclical object graph due to your one-to-many relationship. For those you may use the JSOG serializer plugin for Jackson.

It supposedly handles both serialization and deserialization of cyclical object graphs (since Jackson 2.5.1)

@JsonIdentityInfo(generator=JSOGGenerator.class)
public class Person {
    String name;
    Person secretSanta;
}

Upvotes: 1

Quentin Lagadec
Quentin Lagadec

Reputation: 231

Ok I found the problem.

Finally I need to add the resolver. So I find here an example of implement ObjectIDResolver :

import com.fasterxml.jackson.annotation.ObjectIdGenerator;
import com.fasterxml.jackson.annotation.ObjectIdResolver;

import javax.persistence.EntityManager;

/**
 * @author fta on 20.12.15.
 */

public class EntityIdResolver
        implements ObjectIdResolver {

    private EntityManager entityManager;

    public EntityIdResolver(
            final EntityManager entityManager) {

        this.entityManager = entityManager;

    }

    @Override
    public void bindItem(
            final ObjectIdGenerator.IdKey id,
            final Object pojo) {

    }

    @Override
    public Object resolveId(final ObjectIdGenerator.IdKey id) {

        return this.entityManager.find(id.scope, id.key);
    }

    @Override
    public ObjectIdResolver newForDeserialization(final Object context) {

        return this;
    }

    @Override
    public boolean canUseFor(final ObjectIdResolver resolverType) {

        return false;
    }

}

And next I add in @JsonIdentityInfo:

@Entity
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "idAgence",
        resolver = EntityIdResolver.class,
        scope=Agence.class)
public class Agence {
    // ... 
}

Upvotes: 18

Bruno R&#233;gnier
Bruno R&#233;gnier

Reputation: 361

For your last exception (utf-8), you can change the signature of your controller like below :

@RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Bean> create(HttpServletRequest  request) throws IOException {
    // getting the posted value
    String body = CharStreams.toString(request.getReader());
    Bean bean = new ObjectMapper().readValue(body, service.getBeanClass());
    bean.setId(null);
    Bean saved = service.save(bean);

    URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(saved.getId()).toUri();
    return ResponseEntity.created(location).body(saved);
}

With this solution, you have the entire control of the mapping.

Enjoy

Upvotes: 0

Georgios Syngouroglou
Georgios Syngouroglou

Reputation: 19974

You have two domains: Materiel and Agence. When Jackson tries to deserialize Materiel it calls the method getAgence. This method returns an Agence object which will be deserialized by Jackson also. When Jackson calls the method getMeteriels (of Agence) it returns a Set of Materiel that will deserialize also. The problem is that Jackson will try to deserialize again each Materiel in the Set. A same so question here.

Upvotes: 0

Related Questions