Search code examples
javajsonjacksondeserializationjson-deserialization

Using Jackson to extract information from Property names


I currently receive the following JSON body

{
    "productId": "90000011",
    "offerId": "String",
    "format": "String",
    "sellerId": "String",
    "sellerName": "String",
    "shippingPrice[zone=BE,method=STD]": 0.0,
    "deliveryTimeEarliestDays[zone=BE,method=STD]": 1,
    "deliveryTimeLatestDays[zone=BE,method=STD]": 1,
    "shippingPrice[zone=NL,method=STD]": 0.0,
    "deliveryTimeEarliestDays[zone=NL,method=STD]": 1,
    "deliveryTimeLatestDays[zone=NL,method=STD]": 1
}

As you can see, I have similar properties that differ by zone and method enclosed in square brackets. I don't want to change the code every time a new zone and/or method is introduced. I'm looking for a more dynamic way you deserialize this via Jackson.

Is there a way to automatically deserialize all properties starting with shippingPrice, deliveryTimeEarliestDays and deliveryTimeLatestDays into the following format?

{
    "productId": "90000011",
    "offerId": "String",
    "format": "String",
    "sellerId": "String",
    "sellerName": "String",
    "deliveryModes":[
    {
        "method":"STD"
        "zone":"BE",
        "shippingPrice":0.0,
        "deliveryTimeEarliestDays":1,
        "deliveryTimeLatestDays":1
    },{
        "method":"STD"
        "zone":"NL",
        "shippingPrice":0.0,
        "deliveryTimeEarliestDays":1,
        "deliveryTimeLatestDays":1
    }]
}

My first idea was to use the @JsonAnySetter annotation and put everything in a Map but that still leaves me with manual parsing of the field name.

My Second Idea was to build a custom deserializer where I loop over all attributes and filter out all the ones that start with shippingPrice, deliveryTimeEarliestDays and deliveryTimeLatestDays and map them to the described format above.


Solution

  • In order to achieve the required result, you need to implement deserialization logic yourself, it can't be done only by sprinkling a couple of data binding annotations.

    That's how it can be done.

    Assume here's a POJO that corresponds to your input JSON (to avoid boilerplate code, I'll use Lombok annotations):

    @Getter
    @Setter
    public static class MyPojo {
        private String productId;
        private String offerId;
        private String format;
        private String sellerId;
        private String sellerName;
        @JsonIgnore // we don't want to expose this field to Jackson as is
        private Map<DeliveryZoneMethod, DeliveryMode> deliveryModes = new HashMap<>();
        
        @JsonAnySetter
        public void setDeliveryModes(String property, String value) {
            DeliveryZoneMethod zoneMethod = DeliveryZoneMethod.parse(property);
            DeliveryMode mode = deliveryModes.computeIfAbsent(zoneMethod, DeliveryMode::new);
            
            String name = property.substring(0, property.indexOf('['));
    
            switch (name) {
                case "shippingPrice" -> mode.setShippingPrice(new BigDecimal(value));
                case "deliveryTimeEarliestDays" -> mode.setDeliveryTimeEarliestDays(Integer.parseInt(value));
                case "deliveryTimeLatestDays" -> mode.setDeliveryTimeLatestDays(Integer.parseInt(value));
            }
        }
        
        public Collection<DeliveryMode> getModes() {
            return deliveryModes.values();
        }
    }
    

    Properties productId, offerId, format, sellerId, sellerName would be parsed by Jackson in a regular way.

    And all other properties formatted like "shippingPrice[zone=BE,method=STD]" would be handled by the method annotated with @JsonAnySetter.

    To facilitate extracting and storing information from such properties I've defined a couple of auxiliary classes:

    • DeliveryZoneMethod which contains information about a zone and delivery method as its name suggests (the purpose of this class is to serve as Key in the map deliveryModes).
    • DeliveryMode which is meant to contain all the need information that correspond to a particular zone and method of delivery.

    For conciseness, DeliveryZoneMethod can be implemented as a Java 16 record:

    public record DeliveryZoneMethod(String method, String zone) {
        public static Pattern ZONE_METHOD = Pattern.compile(".+zone=(\\p{Alpha}+).*method=(\\p{Alpha}+)");
        public static DeliveryZoneMethod parse(String str) {
            // "shippingPrice[zone=BE,method=STD]" - assuming the given string has always the same format
            Matcher m = ZONE_METHOD.matcher(str);
            
            if (!m.find()) throw new IllegalArgumentException("Unable to parse: " + str);
            
            return new DeliveryZoneMethod(m.group(1), m.group(2));
        }
    }
    

    And here's how DeliveryMode might look like:

    @Getter
    @Setter
    public static class DeliveryMode {
        private String method;
        private String zone;
        private BigDecimal shippingPrice;
        private int deliveryTimeEarliestDays;
        private int deliveryTimeLatestDays;
        
        public DeliveryMode(DeliveryZoneMethod zoneMethod) {
            this.method = zoneMethod.method();
            this.zone = zoneMethod.zone();
        }
    }
    

    Usage example:

    public static void main(String[] args) throws JsonProcessingException {
        String json = """
            {
                "productId": "90000011",
                "offerId": "String",
                "format": "String",
                "sellerId": "String",
                "sellerName": "String",
                "shippingPrice[zone=BE,method=STD]": 0.0,
                "deliveryTimeEarliestDays[zone=BE,method=STD]": 1,
                "deliveryTimeLatestDays[zone=BE,method=STD]": 1,
                "shippingPrice[zone=NL,method=STD]": 0.0,
                "deliveryTimeEarliestDays[zone=NL,method=STD]": 1,
                "deliveryTimeLatestDays[zone=NL,method=STD]": 1
            }
            """;
        
        ObjectMapper mapper = new ObjectMapper();
        MyPojo myPojo = mapper.readValue(json,  MyPojo.class);
    
        String serializedJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(myPojo);
        
        System.out.println(serializedJson);
    }
    

    Output:

    {
      "productId" : "90000011",
      "offerId" : "String",
      "format" : "String",
      "sellerId" : "String",
      "sellerName" : "String",
      "modes" : [ {
        "method" : "BE",
        "zone" : "STD",
        "shippingPrice" : 0.0,
        "deliveryTimeEarliestDays" : 1,
        "deliveryTimeLatestDays" : 1
      }, {
        "method" : "NL",
        "zone" : "STD",
        "shippingPrice" : 0.0,
        "deliveryTimeEarliestDays" : 1,
        "deliveryTimeLatestDays" : 1
      } ]
    }