4

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.

user1583209
  • 1,637
  • 3
  • 24
  • 42
  • 2
    Does this answer your question? [simplest way to read json from a URL in java](https://stackoverflow.com/questions/4308554/simplest-way-to-read-json-from-a-url-in-java) – Kaustubh Khare Aug 07 '20 at 16:24
  • @KaustubhKhare while related, that dupe isn't going to help OP because the class they want to parse into does not match the structure of the JSON content. – Chris Schaller Aug 26 '20 at 06:09
  • This is closer https://stackoverflow.com/q/22687771/1690217 the issue is that `rates` is an object with the _properties_ that match your `Currency` structure, you need to convert the `rates` object into an array of it's properties. – Chris Schaller Aug 26 '20 at 06:15

5 Answers5

4

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
}
dpr
  • 10,591
  • 3
  • 41
  • 71
  • 1
    @user1583209 as you're working with currency data I'd advice not to use double for the computation. Instead use `BigDecimal` to avoid rounding issues. Something like `new BigDecimal(rates.getString(key))` will probably do – dpr Aug 10 '20 at 14:26
2

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».

2

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() )

Massimo Petrus
  • 1,881
  • 2
  • 13
  • 26
2

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()));
Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
1

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
Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
  • **Note:** I used the Firefox plugin, [HTTP Header Live](https://addons.mozilla.org/en-US/firefox/addon/http-header-live/) in order to get the value for the header, `User-Agent`. – Arvind Kumar Avinash Aug 07 '20 at 21:04