0

I'm developing a REST API using java and spring boot.

There are two entities with One-to-One relation:

User:

@Entity
@Table(name = "users")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;

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

    @OneToOne(fetch = FetchType.LAZY,
            cascade =  CascadeType.ALL,
            mappedBy = "employee")
    @JsonBackReference
    private Company company;

    // constructors, getters & setters...

And Company:

@Entity
@Table(name = "companies")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "company_name", nullable = false)
    private String companyName;

    @Column(name = "company_info", nullable = false)
    private String companyInfo;

    @CreationTimestamp
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", nullable = false)
    private Date createdAt;

    @OneToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "employee_id", nullable = false)
    @JsonManagedReference
    private User employee;

   // constructors, getters & setters...

I want to be able to create Companies via POST method so I've made one in CompanyController (I omit GET methods in controller as they work as expected):

@RestController
@RequestMapping(value = "/api/v1")
public class CompanyController {

    private static final Logger logger = LoggerFactory.getLogger(CompanyController.class);

    @Autowired
    private CompanyRepository companyRepository;

    @PostMapping(path = "company", produces = "application/JSON")
    public ResponseEntity<?> createCompany(@RequestBody Company company){
        logger.info("Request to save new Company: {}", company);

        Company result = companyRepository.save(company);
        return ResponseEntity.ok().body(result);
    }

The createCompany method works fine when I send request with JSON like below:

{
"companyName" : "Big Client Company 2",
"companyInfo" : "unspecified",
"employee" : {
    "id": 17,
    "firstName": "someName",
    "lastName": "someLastName",
    "email": "someEmail@a.ru"
  }
}

However, I want to be able to send JSONs without whole body of employee field but with just an id:

{
"companyName" : "Big Client Company 2",
"companyInfo" : "unspecified",
"employee" : 17
}

When I do like this I get an error:

JSON parse error: Cannot construct instance of model.User (although at least one Creator exists): no int/Int-argument constructor/factory method to deserialize from Number value (17)

So the question is there any way to do this without changing Company class "employee" to String (and getting rid of One-to-One relation)?

I've tried to find examples of writing custom JSON deserializers but haven't succeeded with any suitable for me.

Mist3r
  • 3
  • 2
  • Is this of any use? https://stackoverflow.com/questions/33475222/spring-boot-jpa-json-without-nested-object-with-onetomany-relation – Alan Hay Feb 04 '19 at 21:03
  • @alan-hay I tried and the company was successfully created and saved to DB. Although the JSON that was sent in response contained an empty body of employee, GET request returned company with correct employee body. Upd: Haven't seen that you updated your comment with link. My response was for trying to send `"employee" : { "id" : 17 }` As for the link I've already checked it before posting myself and it didn't work for me. – Mist3r Feb 04 '19 at 21:27

2 Answers2

0

The problem is that your User has mandatory fields that need to be supplied when creating them (firstName, lastName and email).

You have no way to create a User without them using the definitions you have now.

You can use nullable = true on those fields but you might end up with inconsistencies in the data you want to keep in your DB. If letting these fields be empty then I would suggest this approach.

Also, as a side note, please note that mixing the REST controller layer and the repository layer is usually a bad practice. You should separate these layers by a service layer.

João Dias Amaro
  • 511
  • 6
  • 14
  • Unfortunately those fields cannot be null. Thank you for the note! I'm pretty new to Spring boot so will try to find more information about the correct structure and the ways of organizing layers – Mist3r Feb 04 '19 at 21:34
  • No problem :) Check Spring Data REST. I think it will cover all the features you will need. You don't need to define all the APIs behaviour by hand – João Dias Amaro Feb 04 '19 at 22:01
0

You can use custom deserialization:

public class CompanyDeserializer extends StdDeserializer<Company> { 

@Autowired
private UserRepository userRepository;

public CompanyDeserializer() { 
    this(null); 
} 

public CompanyDeserializer(Class<?> vc) { 
    super(vc); 
}

@Override
public Company deserialize(JsonParser jp, DeserializationContext ctxt) 
  throws IOException, JsonProcessingException {
    JsonNode node = jp.getCodec().readTree(jp);
    Company c = new Company();
    //Set the rest of the attributes.
    Long empId = (Long) ((IntNode) node.get("employee")).numberValue();
    c.setEmployee(userRepository.findById(empId).orElse(null)));
    return c;
}

}

JJCV
  • 326
  • 2
  • 19