Spring Jackson deserialize object with reference to existing object by Id

Tags: , , , ,



I have three entity classes of the following:

Shipments Entity:

@Entity
@Table(name = "SHIPMENT")
public class Shipment implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "SHIPMENT_ID", nullable = false)
    private int shipmentId;

    @Column(name = "DESTINATION", nullable = false)
    private String destination;

    @OneToMany(mappedBy = "shipment")
    private List<ShipmentDetail> shipmentDetailList;
    
//bunch of other variables omitted

    public Shipment(String destination) {
        this.destination = destination;
        shipmentDetailList = new ArrayList<>();
    }

Shipment Details Entity:

@Entity
@Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
    private int shipmentDetailId;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID", nullable = false)
    private Product product;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "SHIPMENT_ID", nullable = false)
    private Shipment shipment;

//bunch of other variables omitted 


    public ShipmentDetail() {
    }

    public ShipmentDetail(Shipment shipment, Product product) {
        this.product = product;
        this.shipment = shipment;
    }

Product Entity:

@Entity
@Table(name = "Product")
public class Product implements Serializable {

    @Id
    @Column(name = "PRODUCT_ID", nullable = false)
    private String productId;

    @Column(name = "PRODUCT_NAME", nullable = false)
    private String productName;

//bunch of other variables omitted 

    public Product() {
    }

    public Product(String productId, String productName) {
        this.productId = productId;
        this.productName = productName;
    }

I am receiving JSONs through a rest API. The problem is I do not know how to deserialize a new Shipment with shipmentDetails that have relationships to already existing objects just by ID. I know you can simply deserialize with the objectmapper, but that requires all the fields of product to be in each shipmentDetail. How do i instantiate with just the productID?

Sample JSON received

{
    "destination": "sample Dest",
    "shipmentDetails": [
        {
            "productId": "F111111111111111"
        },
        {
            "productId": "F222222222222222"
        }
    ]
}

Currently my rest endpoint would then receive the JSON, and do this:

public ResponseEntity<String> test(@RequestBody String jsonString) throws JsonProcessingException {
        JsonNode node = objectMapper.readTree(jsonString);
        String destination = node.get("destination").asText();
        Shipment newShipment = new Shipment(destination);
        shipmentRepository.save(newShipment);

        JsonNode shipmentDetailsArray = node.get("shipmentDetails");
        int shipmentDetailsArrayLength = shipmentDetailsArray.size();
        for (int c = 0; c < shipmentDetailsArrayLength; c++) {
            String productId = node.get("productId").asText();
            Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
            ShipmentDetail shipmentDetail = new ShipmentDetail(newShipment, product, quantity);
            shipmentDetailRepository.save(shipmentDetail);
        }
    }

what i want to do is:

public ResponseEntity<String> test2(@RequestBody String jsonString) throws JsonProcessingException {
    
    JsonNode wholeJson = objectMapper.readTree(jsonString);
    Shipment newShipment = objectMapper.treeToValue(wholeJson, Shipment.class);
    
    return new ResponseEntity<>("Transfer Shipment successfully created", HttpStatus.OK);
}

I tried this solution to no. avail: Deserialize with Jackson with reference to an existing object

How do I make the product entity search for an existing product instead of trying to create a new product. The hacky extremely inefficient workaround I have been using is to traverse the json array, and for every productId find the product using the productRepository, and then set the shipmentDetail with the product one by one. Im not sure if this is best practice as im self learning spring.

So in pseudocode what im trying to do would be:

  1. Receive JSON
  2. Instantiate Shipment entity
  3. Instantiate an array of shipmentDetail entities For each shipmentDetail: 1. Find product with given productId 2. Instantiate shipmentDetail with product and shipment

Code has been significantly simplified to better showcase the problem,

Answer

You have a bottleneck in your code in this part:

Product product = productRepository.findById(productId)

Because you are making a query for each productId, and it will perform badly with large number of products. Ignoring that, I will recommend this aproach.

  1. Build your own deserializer (see this):

    public class ShipmentDeserializer extends JsonDeserializer {
         @Override
         public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
                 throws IOException, JsonProcessingException {
             JsonNode node = jp.getCodec().readTree(jp);
             String destination = node.get("destination").asText();
             Shipment shipment = new Shipment(destination);
             JsonNode shipmentDetailsNode = node.get("shipmentDetails");
             List shipmentDetailList = new ArrayList();
             for (int c = 0; c < shipmentDetailsNode.size(); c++) {
                 JsonNode productNode = shipmentDetailsNode.get(c);
                 String productId = productNode.get("productId").asText();
                 Product product = new Product(productId);
                 ShipmentDetail shipmentDetail = new ShipmentDetail(product);
                 shipmentDetailList.add(shipmentDetail);
             }
             shipment.setShipmentDetailList(shipmentDetailList);
             return shipment;
         }
     }
  2. Add the deserializer to your Shipment class:

     @JsonDeserialize(using = ShipmentDeserializer .class)
     public class Shipment {
         // Class code
     }
  3. Deserialize the string:

     public ResponseEntity test2(@RequestBody String jsonString) throws JsonProcessingException {
         Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
         /* More code */
         return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
     }
     
  4. At this point, you are only converting the Json into classes, so we need to persist the data.

     public ResponseEntity test2(@RequestBody String jsonString) throws JsonProcessingException {
         Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
         shipmentRepository.save(newShipment);
         List<ShipmentDetail> shipmentDetails = newShipment.getShipmentDetailList();
         for (int i = 0; i < shipmentDetails.size(); c++) {
             ShipmentDetail shipmentDetail = shipmentDetails.get(i);
             shipmentDetail.setShipment(newShipment);
             Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
             shipmentDetail.setProduct(product);
             shipmentDetailRepository.save(shipmentDetail);
         }
         return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
     }
     

I know you want to reduce the code in the test method, but I DO NOT RECOMMEND to combine the Json deserialize with the persistence layer. But if you want to follow that path, you could move the productRepository.findById(productId) into the ShipmentDeserializer class like this:

public class ShipmentDeserializer extends JsonDeserializer {
        @Override
        public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            JsonNode node = jp.getCodec().readTree(jp);
            String destination = node.get("destination").asText();
            Shipment shipment = new Shipment(destination);
            JsonNode shipmentDetailsNode = node.get("shipmentDetails");
            List shipmentDetailList = new ArrayList();
            for (int c = 0; c < shipmentDetailsNode.size(); c++) {
                JsonNode productNode = shipmentDetailsNode.get(c);
                String productId = productNode.get("productId").asText();
                Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
                ShipmentDetail shipmentDetail = new ShipmentDetail(product);
                shipmentDetailList.add(shipmentDetail);
            }
            shipment.setShipmentDetailList(shipmentDetailList);
            return shipment;
        }
    }

But if you want to do that, you need to inject the repository into the deserializer (see this).



Source: stackoverflow