Search code examples
javajsonkey-valuejson-deserialization

Deserialize JSON - nested lists and key needed as value


I need to deserialize this in Java 1.7 with Jackson, using currency as value:

"prices": {
    "USD": [
        [1, "1.99000"],
        [100, "1.89000"],
        [1000, "1.40500"]
    ],
    "EUR": [
        [1, "1.53000"],
        [100, "1.45000"],
        [1000, "1.08350"]
    ]
}

(source is this REST API https://octopart.com/api/docs/v3/rest-api#notes-partoffer.prices)

My goal is to get objects for each Price with attributes currency, price break and price.

I can't figure out how to perform that. I tried the following:

  1. deserialize the whole "prices" as String for further processing (didn't work, got Can not deserialize instance of java.lang.String out of START_OBJECT token - obviously, since this is JSON_OBJECT)

  2. deserialize like

    LinkedHashMap<String, LinkedHashMap<Integer, String>>>
    

(inspired by this Deserializing Jackson by using a key as value, but also didn't work)

  1. use a wrapper like this:

    public class PartOfferPriceWrapper {
    
    private LinkedHashMap<String, List<Entry<Integer, String>>> prices;
    
    }
    

and then process like:

List<PartOfferPrice> partOfferPriceList = new ArrayList<PartOfferPrice>();

  for (Entry<String, List<Entry<Integer, String>>> currencyEntry : 
    partOfferPrices.entrySet()) {

      for (Entry<Integer, String> priceEntry : currencyEntry.getValue()) {
        PartOfferPrice partOfferPrice = new PartOfferPrice();

        partOfferPrice.setOffer_id(partOfferId);
        partOfferPrice.setCurrency(currencyEntry.getKey());
        partOfferPrice.setPriceBreak(priceEntry.getKey());              

        partOfferPrice.setPrice(Double.parseDouble(priceEntry.getValue()));

        partOfferPriceList.add(partOfferPrice);
        }

This seems fine, but the result is empty (it's a part of bigger response, reading with Response.readEntity was successful). I can't find any other way how to process this (some custom deserializer?).

EDIT

I tried to use custom deserializer following tima's advice:

    public class PartOfferPricesWrapperDeserializer extends 
    JsonDeserializer<PartOfferPricesWrapper> {

    public PartOfferPricesWrapperDeserializer() { super(); }

    @Override
    public PartOfferPricesWrapper deserialize(JsonParser jsonParser, 
        DeserializationContext context) throws IOException, 
        JsonProcessingException {

        PartOfferPricesWrapper partOfferPricesWrapper = new 
        PartOfferPricesWrapper();
        List<PartOfferPrice> priceList = new ArrayList<PartOfferPrice>();

        JsonNode node = jsonParser.readValueAsTree();

        Iterator<Entry<String, JsonNode>> nodes = 
        node.get("prices").fields();

        while (nodes.hasNext()) {               
            Map.Entry<String, JsonNode> entry = nodes.next();               
            for (JsonNode tempNode : entry.getValue()) {

                PartOfferPrice price = new PartOfferPrice();
                price.setCurrency(entry.getKey());

                for (int i = 0; i < tempNode.size(); i++) {

                    if (tempNode.get(i).isInt()) {
                        price.setPriceBreak(tempNode.get(i).intValue());                            
                    }
                    else
                    {
                        price.setPrice(tempNode.get(i).asDouble());                         
                    }                                           
                }

            priceList.add(price);
            }
        }
        partOfferPricesWrapper.setPrices(priceList);
        return partOfferPricesWrapper;
    }
}

adding this to the handler method:

    SimpleModule module = new SimpleModule();
    module.addDeserializer(PartOfferPricesWrapper.class, new 
    PartOfferPricesWrapperDeserializer());
    objectMapper.registerModule(module);          

    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
    false);
    partsMatchResponse = objectMapper.readValue(target.getUri().toURL(), 
    PartsMatchResponse.class);

Which would probably work if the response contains only "prices" node, but now I'm getting NullPointerException on node.get("prices").fields(); It is probably trying to parse the whole response, but I need to use the custom deserializer only for the "prices" part. Is it somehow possible?

Thanks a lot.


Solution

  • Yes, a custom deserializer would work.

    class CustomDeserializer extends JsonDeserializer<Prices> {
    
        public CustomDeserializer() { super(); }
    
        @Override
        public Prices deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
            JsonNode node = jsonParser.readValueAsTree();
    
            Iterator<Entry<String, JsonNode>> nodes = node.get("prices").fields();
    
            while (nodes.hasNext()) {
                Map.Entry<String, JsonNode> entry = nodes.next();
    
                System.out.println(entry.getKey());
    
                for (JsonNode tempNode : entry.getValue()) {
                    for (int i = 0; i < tempNode.size(); i++) {
                        System.out.println(tempNode.get(i).getClass() + "\t" + tempNode.get(i));
                    }
                }
    
                System.out.println();
            }
    
            return null;
        }
    }
    

    Output

    USD
    class com.fasterxml.jackson.databind.node.IntNode   1
    class com.fasterxml.jackson.databind.node.TextNode  "1.99000"
    class com.fasterxml.jackson.databind.node.IntNode   100
    class com.fasterxml.jackson.databind.node.TextNode  "1.89000"
    class com.fasterxml.jackson.databind.node.IntNode   1000
    class com.fasterxml.jackson.databind.node.TextNode  "1.40500"
    
    EUR
    class com.fasterxml.jackson.databind.node.IntNode   1
    class com.fasterxml.jackson.databind.node.TextNode  "1.53000"
    class com.fasterxml.jackson.databind.node.IntNode   100
    class com.fasterxml.jackson.databind.node.TextNode  "1.45000"
    class com.fasterxml.jackson.databind.node.IntNode   1000
    class com.fasterxml.jackson.databind.node.TextNode  "1.08350"
    

    You can create the object and structure that you want in the deserializer (Prices is an empty class I used).

    EDIT

    You can use the same custom deserializer just for the field with a small change.

    The first two lines become like shown below, because you do not need to look for the prices node because when it deserializes the field it will only pass the JSON for that field. The rest of the lines are the same.

    JsonNode node = jsonParser.readValueAsTree();
    Iterator<Entry<String, JsonNode>> nodes = node.fields();
    

    Then in your wrapper class:

    class PricesWrapper {
    
        // ...
    
        @JsonDeserialize(using = CustomDeserializer.class)
        private Prices prices;
    
        // ...
    }