albaba albaba - 23 days ago 22
reST (reStructuredText) Question

Create reference to parent on entity creation via POST in Spring Boot

I'm currently trying to build a REST API with Spring Boot that supports Clients and Jobs. A Client can have many Jobs, and a Job belongs to a Client. However, when I save a Job with a reference to the Client either by a client_id or trying to find the Client object, I get this error:

{
"timestamp" : 1479163164739,
"status" : 400,
"error" : "Bad Request",
"exception" : "org.springframework.http.converter.HttpMessageNotReadableException",
"message" : "Could not read document: Can not construct instance of com.core.model.Client: no String-argument constructor/factory method to deserialize from String value ('3')\n at [Source: java.io.PushbackInputStream@5138cca1; line: 1, column: 12] (through reference chain: com.core.model.Job[\"client\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.core.model.Client: no String-argument constructor/factory method to deserialize from String value ('3')\n at [Source: java.io.PushbackInputStream@5138cca1; line: 1, column: 12] (through reference chain: com.core.model.Job[\"client\"])",
"path" : "/api/v1/jobs"
}


Including my models and controllers for Job and Client:

My model for Job:

package com.core.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.CascadeType;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Id;

@Entity
@Table(name="`job`")
public class Job {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="job_id")
private Long jobId;

@ManyToOne(cascade=CascadeType.ALL, targetEntity = Client.class)
@JoinColumn(name = "client_id", nullable = false)
private Client client;

private String address1;
private String address2;
private String city;
private String county;
private String state;
private String zip;
private String country;
private String description;

//Note: I tried Long client_id instead of using the Client object here and same thing
public Job(String description, String address1, String address2, String city, String county,
String state, String zip, String country, Client client) {
this.setDescription(description);
this.setAddress1(address1);
this.setAddress2(address2);
...
this.setClient(client);
}

public Job() {}

public Long getId() { return jobId; }
public void setId(Long id) { this.jobId = id; }
...
public Client getClient() { return client; }
public void setClient(Client client) { this.client = client; }

}


Controller for the Job:

package com.core.controller;

import com.core.model.Job;
import com.core.repository.JobRepository;
import org.apache.catalina.connector.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.core.repository.JobRepository;
import org.springframework.hateoas.Resource;

import java.util.Collection;
import java.util.List;

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {

@Autowired
private JobRepository repository;

@RequestMapping(method = RequestMethod.GET)
public ResponseEntity<Collection<Job>> getAllJob(){
return new ResponseEntity<>((Collection<Job>) repository.findAll(), HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
public ResponseEntity<Job> getEquipmentWithId(@PathVariable Long id) {
return new ResponseEntity<>(repository.findOne(id),HttpStatus.OK);
}

@RequestMapping(value="", method=RequestMethod.POST)
public ResponseEntity createNew(@RequestBody Job job) {
repository.save(job);
return new ResponseEntity(job, HttpStatus.CREATED);
}
}


and Client model:

package com.core.model;

import javax.persistence.Entity;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
@Table(name="`client`")
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="client_id")
@Access(AccessType.PROPERTY)
private Long clientId;

public Client(String description) {
this.setDescription(description);
}

public Client() {}

public Long getClientId() {
return clientId;
}
public void setClientId(Long id) { this.clientId = id; }
...
}


I just want to save that Client id to the Job, what's the right approach here?

Answer

My guess is that you're trying to invoke this method using REST:

@RequestMapping(value="", method=RequestMethod.POST)
public ResponseEntity createNew(@RequestBody Job job) {
        repository.save(job);
        return new ResponseEntity(job, HttpStatus.CREATED);
}

Th exception is pretty clear, it talks about:

com.fasterxml.jackson.databind.JsonMappingException

Which means that whenever you're trying to pass a Job entity within the request body, Jackson is failing to bind it to your Job job parameter on your method controller.

My suggestion is to try to write Jackson deserializer class, which will tell Jackson how exactly to do the deserialization.

Here's an example of how to do it:

Right way to write JSON deserializer in Spring or extend it