Home > Back-end >  Use jackson to extract information from field names
Use jackson to extract information from field names

Time:11-08

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 'duplicate' attributes but for a different zone and method. 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.

CodePudding user response:

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
  } ]
}

CodePudding user response:

I would go with your first idea to deserialize your JSON into a map. And yes you will still need to analyze the map keys. It is easy to deserialize Json into a map with Json Jackson, but there is an Open source library called MgntUtils that provides class JsonUtils which is a thin wrapper over Json-Jackson library. using it you can very simply deserialize your Json into a Map (or any other class). Your code would look like this:

try {
            Map<String, Object> map = JsonUtils.readObjectFromJsonString(jsonStr, Map.class);
            System.out.println(map);
    } catch (IOException e) {
      ...
    }

Here is Javadoc for JsonUtils. The library can be obtained as maven artifact or on Github (with source code and Javadoc).
Disclaimer: This library is written and maintained by me

  • Related