JSONObject to ArrayList square brackets missing

Viewed 710

I am trying to get the rates from https://api.ratesapi.io/api/latest into an ArrayList<Currency> of a custom Currency class:

public class Currency {
    private String shortName;
    private double rate;
    ...
}

The JSON looks like:

{"base":"EUR","rates":{"GBP":0.90033,"HKD":9.1786,"IDR":17304.0,
 "ILS":4.0309,"DKK":7.45,"INR":88.765,"CHF":1.0759,"MXN":26.615,
 "CZK":26.202,"SGD":1.6236,"THB":36.832,"HRK":7.468,"MYR":4.9604,
 "NOK":10.6538,"CNY":8.2325,"BGN":1.9558,"PHP":58.136,"SEK":10.3165,
 "PLN":4.4073,"ZAR":20.7655,"CAD":1.5748,"ISK":160.2,"BRL":6.334,
 "RON":4.836,"NZD":1.7828,"TRY":8.5853,"JPY":124.96,"RUB":86.9321,
 "KRW":1404.99,"USD":1.1843,"HUF":346.23,"AUD":1.6492},"date":"2020-08-06"}

Using org.json I managed to get the data into a JSONObject:

JSONObject obj = new JSONObject(getJSON("https://api.ratesapi.io/api/latest"));

As far as I understand, the normal procedure is now to convert the JSONObject into a JSONArray. However trying:

JSONArray jsonArray = obj.getJSONArray("rates");

fails with the error message:

Exception in thread "main" org.json.JSONException: JSONObject["rates"]
is not a JSONArray.

How do I fix this error or is there another way to make an ArrayList out of the JSON?

I suspect that the problem are missing square brackets in the JSON string.

5 Answers

If you take a look at the JSON returned by the API, you get a JSON object:

{"base":"EUR","rates":{"GBP":0.90033,"HKD":9.1786, ... },"date":"2020-08-06"}

You probably want to do something like this:

JSONObject obj = new JSONObject(getJSON("https://api.ratesapi.io/api/latest"));
JSONObject rates = obj.getJSONObject("rates");
final Iterator<String> keys = rates.keys();
while (keys.hasNext()) {
  final String key = keys.next();
  final Currency currency = new Currency(key, rates.getDouble(key));
  // do something with the Currency
}

You can use ObjectMapper class to convert json from some URL to some kind of object. In this case (if json structure is always the same) it can be Map<String, Object>.

ObjectMapper mapper = new ObjectMapper();
URL url = new URL("https://api.ratesapi.io/api/latest");
Map<String, Object> map = mapper.readValue(url, Map.class);

System.out.println(map);
// {base=EUR, rates={GBP=0.90373, HKD=9.1585, ... , AUD=1.6403}, date=2020-08-07}

Then you can get inner rates map, and (if it is needed) convert it to list using java stream api:

Map<String, Double> rates = (Map<String, Double>) map.get("rates");

System.out.println(rates); // {GBP=0.90373, HKD=9.1585, ... , AUD=1.6403}

Convert Map<String, Object> to ArrayList<Currency>:

ArrayList<Currency> list = rates.entrySet().stream()
    .map(entry -> new Currency(entry.getKey(), entry.getValue()))
    .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

System.out.println(list); // [GBP=0.90373, HKD=9.1585, ... , AUD=1.6403]

Note: add a constructor with two fields shortName and rate;
Note: override the toString method as follows: shortName + "=" + rate;


Maven dependency:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.11.2</version>
</dependency>

See also: «Formatting Json Response into an Array Java».

The object "rates" is not a JSONArray, is a JSONObject.

So you have to do obj.getJSONObject(rates");then iterate on the fields of the JSONObject using map methods (for examply using keySet() )

A working solution using Jackson library and Lombok may be as follows:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.*;
import java.util.*;
import java.util.stream.Collectors;

public class CcyApiParser {
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public static class Currency {
        private String shortName;
        private double rate;
    }

    @Getter
    @Setter
    public static class RatesApiResponse {
        private String base;
        private Map<String, Double> rates;
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
        private LocalDate date;
    }


    public static void main(String[] args) throws IOException {

        ObjectMapper mapper = new ObjectMapper()
                .registerModule(new JavaTimeModule()); // to parse date

        URL apiUrl = new URL("https://api.ratesapi.io/api/latest");
    
        // read proper api response
        RatesApiResponse rates = mapper.readValue(apiUrl, RatesApiResponse.class);

        // convert inner rates into list of Currency objects
        List<Currency> ccys = rates.getRates().entrySet().stream()
                .map(e -> new Currency(e.getKey(), e.getValue()))
                .collect(Collectors.toList());

        ccys.forEach(ccy -> System.out.printf("%s=%s%n", ccy.getShortName(), ccy.getRate()));
    }
}

Output

GBP=0.90033
HKD=9.1786
IDR=17304.0
ILS=4.0309
... etc.

Update

It is also possible to customize deserialization of RatesApiResponse and move mapping of "rates" into this class to convert immediately into list of currencies.

    @Getter
    @Setter
    public static class RatesApiResponse {
        private String base;
        @JsonProperty(access = JsonProperty.Access.READ_ONLY)
        private List<Currency> ccys;

        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
        private LocalDate date;

        // no getter for rates
        // this customized setter for the map of rates converts into a list
        @JsonProperty("rates")
        public void setRates(Map<String, Double> rates) {
            ccys = rates.entrySet().stream()
                    .map(e -> new Currency(e.getKey(), e.getValue()))
                    .collect(Collectors.toList());
        }
    }

// Updates in the test method
RatesApiResponse rates = mapper.readValue(src, RatesApiResponse.class);

rates.getCcys().forEach(ccy -> System.out.printf("%s=%s%n", ccy.getShortName(), ccy.getRate()));

Exception in thread "main" org.json.JSONException: JSONObject["rates"] is not a JSONArray.

You got this error because rates is not in the form of an array. It is simply an element like base and date but looks like an array. Get it from the JSON string like you get base and date from it and then process it to create the required List<Currency>.

Given below is the working code with the explanation added as comments in the code:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

class Currency {
    private String shortName;
    private double rate;

    public Currency(String shortName, double rate) {
        this.shortName = shortName;
        this.rate = rate;
    }

    @Override
    public String toString() {
        return shortName + ":" + rate;
    }
}

public class Main {

    public static JSONObject getJSON(String url) throws IOException, JSONException {
        // Create a URLConnection for the given URL
        URLConnection connection = new URL(url).openConnection();

        // Add header to avoid 403 Forbidden HTTP status code
        connection.addRequestProperty("User-Agent",
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:79.0) Gecko/20100101 Firefox/79.0" + "");

        StringBuilder jsonStr = new StringBuilder();

        // Get InputStream from connection and read the response
        try (InputStream is = connection.getInputStream();) {
            Reader reader = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));

            int ch;
            while ((ch = reader.read()) != -1) {
                jsonStr.append((char) ch);
            }
        }
        return new JSONObject(jsonStr.toString());
    }

    public static void main(String[] args) throws IOException, JSONException {
        JSONObject jsonObj = getJSON("https://api.ratesapi.io/api/latest");

        // Get rates from jsonObj
        String rates = jsonObj.get("rates").toString();

        // Remove {, }, and " from the string
        String[] keyValArr = rates.replaceAll("[\\{\\\"}]", "").split(",");

        // List object to hold Currency objects
        List<Currency> list = new ArrayList<>();

        for (String keyVal : keyValArr) {
            // Split each key:value string on ':'
            String[] curRate = keyVal.split(":");

            // Add Currency object to List
            list.add(new Currency(curRate[0], Double.parseDouble(curRate[1])));
        }

        // Display list
        list.forEach(System.out::println);
    }
}

Output:

CHF:1.0804
HRK:7.4595
MXN:26.5127
...
...
...
NZD:1.7786
BRL:6.3274
Related