Manuel Páez Manuel Páez - 5 months ago 105
Java Question

Spring HATEOAS/Jackson with resources and resource assembler

I got a Jackson error because of a nested loop in a bidirectional relationship of two of my JPA entities (Task and Job). I started researching and partially managed to solve this using the @JsonManagedReference and @JsonBackReference annotations, but this method only works when I use the annotations within my entities, which ends with the JSON serialization jumping over my JobResource/JobResourceAssembler and TaskResource/TaskResourceAssembler, getting a HATEOASless and HALless json response.

Is there a way to get this serialization/deserialization being managed by my resources?

Some code (this way I got a json response but at exchange of being HATEOASless and HALless):

@Entity
public class Task {

@Id
@GeneratedValue
private Long id;

@OneToMany(mappedBy="task")
@JsonManagedReference
private List<Job> job = new ArrayList<Job>();

//constructors, getter, setter...

@Entity
public class Job {

@Id
@GeneratedValue
private Long id;

@ManyToOne
@JsonBackReference
@JoinColumn(name = "task_id", updatable = true, insertable = true, nullable = true)
private Task task;

//constructor, setter, getter.


HATEOASless response (jobs should have links)

{
"_embedded": {
"tasks": [
{
"name": "Task",
"description": "Task Description",
"createdAt": 1467583658749,
"updatedAt": null,
"deletedAt": null,
"isActive": true,
"estimatedStartDate": null,
"startDate": null,
"estimatedDateEnd": null,
"dateEnd": null,
"ids": 1,
"risk": null,
"job": [
{
"id": 2,
"name": "Job",
"description": "Job Description",
"createdAt": 1467583673859,
"updatedAt": null,
"deletedAt": null,
"isActive": true
},
{
"id": 3,
"name": "Job2",
"description": "Job Description",
"createdAt": 1467583676138,
"updatedAt": null,
"deletedAt": null,
"isActive": true
},
{
"id": 4,
"name": "Job3",
"description": "Job Description",
"createdAt": 1467583679339,
"updatedAt": null,
"deletedAt": null,
"isActive": true
}
],
"_links": {
"self": {
"href": "http://127.0.0.3:7890/api/v1/tasks/1"
},
"task": {
"href": "http://127.0.0.3:7890/api/v1/tasks/1"
}
}
}
]

Answer

I solved this with my coworker. First: the problem was that jackson was serializing our related entities directly, ignoring spring HATEOAS' resources. This was caused because our resources were being feeded by object list, no resources list, so we changed that:

@Relation(collectionRelation = "tasks")
public class TaskResource extends ResourceSupport {

    private List<JobResource> job = new ArrayList<JobResource>();

Now, with my resource list being feeded by a resource and not a regular entity, we made a service which could fill the List job with regular jobs. Being the case that a entity has its own resource which is basically the same, the process was fairly fast to develop:

public List<JobResource> findJobsFromTask(Long id) {

    Task task = taskRepository.findOne(id);
    List<Job> jobs = task.getJob(); 
    List<JobResource> jobResourceList = new ArrayList<JobResource>();

    for (Job job : jobs) {
        jobResourceList.add(new JobResourceAssembler().toResource(job));
    }
    return jobResourceList;
}

Having this, we just needed to fix the assembler so it will add the new JobResource:

@Override
public TaskResource toResource(Task task) {
    taskResource.setJob(taskService.findJobsFromTask(task.getId()));
}

And this was our solution. If there's a better one, please leave comment or another answer to discuss it.

Edit: it may be necessary to manually add everything to the resource when there're 3+ nested resources. Let's day we have 3 resources: project -> task -> job. If we did the previous approach, then it will end with a nested null error. The service:

@Override
public List<TaskResource> findTaskFromProject(Long id) {

    Project project = projectRepository.findOne(id);
    List<Task> tasks = project.getTask();
    List<TaskResource> taskResourceList = new ArrayList<TaskResource>();

    for (Task task : tasks) {
        TaskResource taskResource = new TaskResource();
        taskResource.setName(task.getName());
        taskResource.setDescription(task.getDescription());
        taskResource.setCreatedAt(task.getCreatedAt());
        taskResource.setUpdatedAt(task.getUpdatedAt());
        taskResource.setDeletedAt(task.getDeletedAt());
        taskResource.setIsActive(task.getIsActive());
        taskResource.setRisk(task.getRisk());
        //taskResource.setDocumentState(task.getDocumentState());
        taskResource.setEstimatedStartDate(task.getEstimatedStartDate());
        taskResource.setStartDate(task.getStartDate());
        taskResource.setEstimatedDateEnd(task.getEstimatedDateEnd());
        taskResource.setDateEnd(task.getDateEnd());
        taskResource.setIds(task.getId());
        taskResource.setJob(taskService.findJobsFromTask(task.getId()));
        taskResource.add(linkTo(TaskController.class).slash("").slash(task.getId()).withSelfRel());
        taskResource.add(linkTo(TaskController.class).slash("").slash(task.getId()).withRel("task"));

        taskResourceList.add(taskResource);
    }

    return taskResourceList;
}

With this, we obtained a project -> task -> job JSON response full hateoas /hal.