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.
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
}
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()));
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».
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
I've downloaded a large amount of historic crypto market data via an API. It is formatted like this:
[
[1601510400000,"4.15540000","4.16450000","4.15010000","4.15030000","4483.01000000",1601510459999,"18646.50051400",50,"2943.27000000","12241.83706500","0"],
...
[1609490340000,"4.94020000","4.95970000","4.93880000","4.94950000","5307.62000000",1609490399999,"26280.03711000",98,"3751.46000000","18574.22402400","0"]
]
I take that to be an array of arrays, the inner one containing heterogeneous types (always the same types in the same order). As an intermediate step I've saved it to text files but I'd like to read it back and map it onto an array of objects of this type:
public class MinuteCandle {
private long openTime;
private double openValue;
private double highValue;
private double lowValue;
private double closeValue;
private double volume;
private long closeTime;
private double quoteAssetVolume;
private int numberOfTrades;
private double takerBuyBaseAssetVolume;
private double takerBuyQuoteAssetVolume;
private double someGarbageData;
//...
}
I'm using the Spring Framework and the jackson library for json mapping. Is it doable with that or should I manually parse the text somehow?
Use JsonFormat and annotate your class with it where you specify shape as an ARRAY:
#JsonFormat(shape = JsonFormat.Shape.ARRAY)
class MinuteCandle
Also, consider to use BigDecimal instead of double if you want to store a price.
See also:
A realistic example where using BigDecimal for currency is strictly
better than using double
How to deserialise anonymous array of mixed types with Jackson
I would do this in two steps:
Read the JSON content into a list of List<Object> with Jackson.
Convert each List<Object> into a MinuteCandle object
and collect these objects into a list of MinuteCandles.
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
File file = new File("example.json");
List<List<Object>> lists = objectMapper.readValue(file, new TypeReference<List<List<Object>>>() {});
List<MinuteCandle> minuteCandles = new ArrayList<>();
for (List<Object> list : lists) {
minuteCandles.add(MinuteCandle.createFromList(list));
}
}
}
The conversion from List<Object> to MinuteCandle (step 2 from above)
could be achieved by adding a static method in your MinuteCandle class.
public static MinuteCandle createFromList(List<Object> list) {
MinuteCandle m = new MinuteCandle();
m.openTime = (Long) list.get(0);
m.openValue = Double.parseDouble((String) list.get(1));
m.highValue = Double.parseDouble((String) list.get(2));
m.lowValue = Double.parseDouble((String) list.get(3));
m.closeValue = Double.parseDouble((String) list.get(4));
m.volume = Double.parseDouble((String) list.get(5));
m.closeTime = (Long) list.get(6);
m.quoteAssetVolume = Double.parseDouble((String) list.get(7));
m.numberOfTrades = (Integer) list.get(8);
m.takerBuyBaseAssetVolume = Double.parseDouble((String) list.get(9));
m.takerBuyQuoteAssetVolume = Double.parseDouble((String) list.get(10));
m.someGarbageData = Double.parseDouble((String) list.get(11));
return m;
}
Assuming the text stored in the file is valid JSON, similar to the solution in How to Read JSON data from txt file in Java? one can use com.google.gson.Gson as follows :
import com.google.gson.Gson;
import java.io.FileReader;
import java.io.Reader;
public class Main {
public static void main(String[] args) throws Exception {
try (Reader reader = new FileReader("somefile.txt")) {
Gson gson = new Gson();
MinuteCandle[] features = gson.fromJson(reader, MinuteCandle[].class);
}
}
}
I want to work with crypto-stock data described here in my spring boot application. The RESTTemplate uses Gson for deserialization. Response data looks like:
{
"IOST": {
"EUR": 0.01147,
"USD": 0.01296
},
"XRP": {
"EUR": 0.2837,
"USD": 0.3208
},
...
}
I have already already written a custom deserializer before. The problem is, that this comes as a single object with key-value pairs insted of as an array. The result should be a list of following objects:
public class Symbol {
private Long id; // not relevant during conversion
private Date timestamp; // not relevant during conversion
private String symbol;
private Double eurPrice;
private Double usdPrice;
}
Any idea how this can be accomplished this?
Because response from this API is dynamic and depends from parameters the best choice is to use dynamic structure on deserialisation side. The best choice is Map. As keys you can use String or enum: Currency, Crypto. After deserialisation you can convert Map to required POJO class. Simple example:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GsonTest {
public static void main(String[] args) {
String json = "{\"IOST\": {"
+ " \"EUR\": 0.01147,"
+ " \"USD\": 0.01296"
+ " },"
+ " \"XRP\": {"
+ " \"EUR\": 0.2837,"
+ " \"USD\": 0.3208"
+ "}}";
Gson gson = new GsonBuilder().create();
Type type = new TypeToken<Map<String, Map<Currency, BigDecimal>>>() {
}.getType();
Map<String, Map<Currency, BigDecimal>> response = gson.fromJson(json, type);
List<Symbol> symbols = response.entrySet()
.stream()
.map(e -> {
Symbol symbol = new Symbol();
symbol.setSymbol(e.getKey());
symbol.setEurPrice(
e.getValue().getOrDefault(Currency.EUR, BigDecimal.ZERO).doubleValue());
symbol.setUsdPrice(
e.getValue().getOrDefault(Currency.USD, BigDecimal.ZERO).doubleValue());
return symbol;
}).collect(Collectors.toList());
System.out.println(symbols);
}
enum Currency {
EUR,
USD
}
}
Above example prints:
[Symbol{id=null, timestamp=null, symbol='IOST', eurPrice=0.01147, usdPrice=0.01296}, Symbol{id=null, timestamp=null, symbol='XRP', eurPrice=0.2837, usdPrice=0.3208}]
If you want to create directly List of Symbol-s you need to implement custom deserialiser. For example, take a look on this question: Parsing JSON Array to Java List Using Gson
You have to make a class as below
public class Symbol {
private double EUR;
private double USD;
public double getEUR() {
return EUR;
}
public void setEUR(double EUR) {
this.EUR = EUR;
}
public double getUSD() {
return USD;
}
public void setUSD(double USD) {
this.USD = USD;
}
}
Above is a model and below is parsing
try {
HashMap<String, Symbol> multiMap = new HashMap<String, Symbol>();
JSONObject mJsonObject = new JSONObject(mResponse);
Iterator<String> iter = mJsonObject.keys();
while (iter.hasNext()) {
String key = iter.next();
try {
Symbol msymbol = new Symbol();
msymbol.setEUR(mJsonObject.getJSONObject(key).getDouble("EUR"));
msymbol.setEUR(mJsonObject.getJSONObject(key).getDouble("USD"));
multiMap.put(key, msymbol);
} catch (JSONException e) {
// Something went wrong!
}
}
}catch (Exception e){
e.printStackTrace();
}
Hareshs anweser didn't fit 100% but it helped me figuring it out:
var url = "...";
var entries = new ArrayList<Symbol>();
var timestamp = Timestamp.from(Instant.now());
Symbol symbol;
ResponseEntity<JsonObject> response = restTemplate.getForEntity(url, JsonObject.class);
for (Map.Entry<String, JsonElement> entry : response.getBody().entrySet()) {
symbol = new Symbol();
symbol.setSymbol(entry.getKey());
symbol.setEurPrice(entry.getValue().getAsJsonObject().get("EUR").getAsDouble());
symbol.setUsdPrice(entry.getValue().getAsJsonObject().get("USD").getAsDouble());
symbol.setTimestamp(timestamp);
entries.add(symbol);
}
This does the trick. However, if anybody finds a way to do this completely within a Gson deserializer I would love to hear it!
I have seen answers like this one that show the use of TypeFactory.constructMapType(...) to de-serialise a JSON string to a map where the key/value combinations are other than String. I have a situation where I have strings that should de-serialise to multiple different types, not just one.
I realise that one solution would be define my own class and not use Map, but I am wondering if I can use pure configuration instead?
Here is my test code.
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.joda.JodaModule;
public class JodaTimeMapTest {
public static void main(final String[] args) throws Exception {
// Map with dates.
final DateTime now = new DateTime().withZone(DateTimeZone.UTC);
final LocalDateTime nowLocal = new LocalDateTime();
final LocalDateTime notNowLocal = new LocalDateTime(2007, 3, 25, 2, 30, 0);
final Map<String, Object> dateMap = new HashMap<>();
dateMap.put("now", now);
dateMap.put("nowLocal", nowLocal);
dateMap.put("notNowLocal", notNowLocal);
// Serialise map to string.
final ObjectMapper mapper = mapper();
final String dateMapJson = mapper.writeValueAsString(dateMap);
// De-serialise string to map.
final TypeFactory typeFactory = mapper.getTypeFactory();
final MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class);
final HashMap<String, Object> dateMapFromJson = mapper.readValue(dateMapJson, mapType);
// First one has dates, second has strings.
printMap(dateMap);
printMap(dateMapFromJson);
}
private static void printMap(final Map<String, Object> map) {
System.out.println(map.entrySet().stream().map(entry -> {
return entry.getKey() + ", type = " + entry.getValue().getClass().getName() + ", value = " + entry.getValue();
}).collect(Collectors.joining(System.lineSeparator())));
}
private static ObjectMapper mapper() {
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return mapper;
}
}
The output of this class shows that reading in, Jakcson can only assume these are strings:
now, type = org.joda.time.DateTime, value = 2018-05-04T09:10:26.063Z
notNowLocal, type = org.joda.time.LocalDateTime, value = 2007-03-25T02:30:00.000
nowLocal, type = org.joda.time.LocalDateTime, value = 2018-05-04T19:10:26.193
now, type = java.lang.String, value = 2018-05-04T09:10:26.063Z
notNowLocal, type = java.lang.String, value = 2007-03-25T02:30:00.000
nowLocal, type = java.lang.String, value = 2018-05-04T19:10:26.193
Sample Solution
Based on the answer aussie gave, here is a solution that works for me. In my example, the map key is all I need to determine what sort of Joda date/time class the value is.
First is my implementation of the de-serialiser aussie told me about.
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/** De-serialise values from a map that contains Joda times and strings. */
public class JodaMapDeserialiser extends StdDeserializer<Object> {
/** Mapping between keys in the map to a type of Joda time. */
enum DateType {
DATE_TIME("now"), LOCAL_DATE_TIME("notNowLocal", "nowLocal");
final List<String> keys;
DateType(final String... keys) {
this.keys = Arrays.asList(keys);
}
public static DateType forKeyString(final String keyString) {
return Stream.of(values()).filter(dateTypes -> dateTypes.keys.contains(keyString)) //
.findFirst().orElse(null);
}
}
public JodaMapDeserialiser() {
super(Object.class);
}
#Override
public Object deserialize(final JsonParser p, final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// Each entry in the map has a key and value.
final String value = p.readValueAs(String.class);
final String key = p.getCurrentName();
// Convert the value depending on what the key is.
switch (DateType.forKeyString(key)) {
case DATE_TIME:
return DateTime.parse(value);
case LOCAL_DATE_TIME:
return LocalDateTime.parse(value);
default:
return value;
}
}
}
And here is some slightly revised testing code.
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.joda.JodaModule;
public class JodaTimeMapTest {
public static void main(final String[] args) throws Exception {
// Map with dates.
final DateTime now = new DateTime().withZone(DateTimeZone.UTC);
final LocalDateTime nowLocal = new LocalDateTime();
final LocalDateTime notNowLocal = new LocalDateTime(2007, 3, 25, 2, 30, 0);
final Map<String, Object> dateMap = new HashMap<>();
dateMap.put("now", now);
dateMap.put("nowLocal", nowLocal);
dateMap.put("notNowLocal", notNowLocal);
// Serialise map to string.
final ObjectMapper mapper = mapper();
final String dateMapJson = mapper.writeValueAsString(dateMap);
// De-serialise string to map.
final TypeFactory typeFactory = mapper.getTypeFactory();
final MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class);
final HashMap<String, Object> dateMapFromJson = mapper.readValue(dateMapJson, mapType);
// First one has dates, second has strings.
System.out.println("Actual map.");
printMap(dateMap);
System.out.println("Map de-serialised from JSON.");
printMap(dateMapFromJson);
System.out.println("Maps are equal: " + dateMap.equals(dateMapFromJson));
}
private static void printMap(final Map<String, Object> map) {
System.out.println(map.entrySet().stream().map(entry -> {
return " " + entry.getKey() + ", type = " + entry.getValue().getClass().getName() + ", value = "
+ entry.getValue();
}).collect(Collectors.joining(System.lineSeparator())));
}
private static ObjectMapper mapper() {
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
final SimpleModule dateDeserializerModule = new SimpleModule();
dateDeserializerModule.addDeserializer(Object.class, new JodaMapDeserialiser());
mapper.registerModule(dateDeserializerModule);
return mapper;
}
}
And the output is:
Actual map.
now, type = org.joda.time.DateTime, value = 2018-05-05T04:03:20.684Z
notNowLocal, type = org.joda.time.LocalDateTime, value = 2007-03-25T02:30:00.000
nowLocal, type = org.joda.time.LocalDateTime, value = 2018-05-05T14:03:20.809
Map de-serialised from JSON.
now, type = org.joda.time.DateTime, value = 2018-05-05T04:03:20.684Z
notNowLocal, type = org.joda.time.LocalDateTime, value = 2007-03-25T02:30:00.000
nowLocal, type = org.joda.time.LocalDateTime, value = 2018-05-05T14:03:20.809
Maps are equal: true
Finally, my maven dependencies (joda time is included in jackson-datatype-joda).
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.5</version>
</dependency>
Other options
Overall, the options I found:
Create type definition for a single type combination: Hashmap with String keys and DateTime values.
Create a custom class to map key/values to.
Create a de-serialiser to define rules for how to translate string to object.
To further explore the different options I found, I wrote up this blog post.
Your date objects are serialized as string thanks to the Jodamodule that you registered: "now":"2018-05-04T11:42:15.454Z"
When you deseriallize the Json string you expect a HashMap with String keys and Object values. How would Jackson know that those objects should be different type of dates, it sees only strings..?
What you could do is to create a custom deserializer for this and implement the logic to deserialize each date correctly (for example you could determine the type by regex).
public class MyDateDeserializer extends StdDeserializer<Object> {
public MyDateDeserializer() {
super(Object.class);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return convertStringToTheProperDate(p.readValueAs(String.class));
}
private Object convertStringToTheProperDate(String dateAsString) {
// implement the logic to convert the string to the proper type
return null;
}
}
And then register the deserializer:
SimpleModule dateDeserializerModule = new SimpleModule();
dateDeserializerModule.addDeserializer(Object.class, new MyDateDeserializer());
mapper.registerModule(dateDeserializerModule);
I'm trying to convert following string to an object,
{
"base":"TRY",
"date":"2017-01-06",
"rates":{
"EUR":0.37679,
"USD":0.51059,
"BRL":0.88513,
"CAD":0.36651,
...
}
}
I know, if I create an object containing all rates as Double attribute, I will be able to convert that object into an object. But I want rates to be in an array like below. How can I create convertedJsonObjectArrayList. Thanks in advance.
List<Rate> rates = convertedJsonObjectArrayList;
class Rate{
String name; //EUR
Double value; //0.37679
}
This assumes that you're guaranteed to have content within the JSON object associated to the name "rates" that will be parseable as a List.
Define a custom deserializer to consume the name-value tokens as pairs
class RatesJsonObjectToArray extends JsonDeserializer<List<Rate>> {
#Override
public List<Rate> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
List<Rate> rates = new ArrayList<>();
// consume two tokens at a time, a name and a value
for (;;) {
String name = p.nextFieldName();
if (name == null) {
break; // no more input
}
JsonToken doubleValueToken = p.nextValue();
if (doubleValueToken != JsonToken.VALUE_NUMBER_FLOAT) { // there's also VALUE_NUMBER_INT for more flexibility
throw new JsonParseException("Expected a numeric value.");
}
double value = p.getDoubleValue();
rates.add(new Rate(name, value));
}
return rates;
}
}
Then annotate your rates field/setter with this JsonDeserializer
#JsonProperty
#JsonDeserialize(using = RatesJsonObjectToArray.class)
private List<Rate> rates;
You can get fancy and write a custom deserializer.
However, if you don't mind being a bit more quick-and-dirty, you can just deserialize to a Map, and convert to your preferred structure explicitly:
String ratesAsJson = "{ \"EUR\" : 0.2, \"USD\":1.0 }";
ObjectMapper objectMapper = new ObjectMapper();
TypeReference<Map<String, Double>> typeRef = new TypeReference<Map<String, Double>>() {};
Map<String, Double> map = objectMapper.readValue(ratesAsJson, typeRef);
List<Rate> list = map.entrySet().stream()
.map(entry -> new Rate(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
(I've assumed a 2-arg constructor on the Rate class).
If you write a custom deserializer, it will have very similar code to the above within it.
I've used a stream and map(). You could use a for loop, but hey, it's 2017!
If you want to convert the currency object(json) into an array(java) You can not use serialization(gson/jaxb) you would need to parse (JsonParser) json and make the conversion yourself
Here is my solution;
ObjectMapper mapper = new ObjectMapper();
CurrencyServiceResponse currencyResp =
mapper.readValue(jsonStr,new TypeReference<CurrencyServiceResponse>(){});
currencyResp.setRates(new ArrayList<>());
for(Map.Entry<String,Double> entry: currencyResp.getRateMap().entrySet()){
currencyResp.getRates().add(
new CurrencyServiceResponse.Rate(entry.getKey(),entry.getValue()));
}
And class for conversion
public class CurrencyServiceResponse {
#JsonProperty("base")
private String base;
#JsonProperty("date")
private Date date;
#JsonProperty("rates")
private Map<String,Double> rateMap;
#JsonIgnore
private List<Rate> rates;
//getters&setters
public static class Rate{
private String name;
private Double value;
public Rate(String name, Double value) {
this.name = name;
this.value = value;
}
//getters&setters
}
}
You can use GSON library. It's the Google JSON library.
JsonParser jp = new JsonParser();
JsonElement root = jp.parse(YOUR_DATA);
JsonObject rootobj = root.getAsJsonObject();
And in your case to access to the EUR rate you can use :
Double rate_eur = rootobj.get("rates").getAsJsonObject().get("EUR").getAsDouble();
Don't forget to import classes :
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
And dependencies :
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>RELEASE</version>
</dependency>
for maven for example.
Would it be possible if someone could help me parse this json result. I have retrieved the result as a string
{"query":{"latitude":39.9889,"longitude":-82.8118},"timestamp":1310252291.861,"address":{"geometry":{"coordinates":[-82.81168367358264,39.9887910986731],"type":"Point"},"properties":{"address":"284 Macdougal Ln","distance":"0.02","postcode":"43004","city":"Columbus","county":"Franklin","province":"OH","country":"US"},"type":"Feature"}}
Jackson. Simple and intuitive to use. For me the best available. Start out with Simple Data Binding, it will throw everything it finds in Maps and Lists.
Like this:
ObjectMapper mapper = new ObjectMapper();
Map<String,Object> yourData = mapper.readValue(new File("yourdata.json"), Map.class);
That's all that's needed.
A good and quick introduction can be found here
And a full working example with your actual data:
import java.io.IOException;
import java.util.Map;
import org.codehaus.jackson.map.ObjectMapper;
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Map<?,?> rootAsMap = mapper.readValue(
"{\"query\":{\"latitude\":39.9889,\"longitude\":-82.8118},\"timestamp\":1310252291.861,\"address\":{\"geometry\":{\"coordinates\":[-82.81168367358264,39.9887910986731],\"type\":\"Point\"},\"properties\":{\"address\":\"284 Macdougal Ln\",\"distance\":\"0.02\",\"postcode\":\"43004\",\"city\":\"Columbus\",\"county\":\"Franklin\",\"province\":\"OH\",\"country\":\"US\"},\"type\":\"Feature\"}}".getBytes(),
Map.class);
System.out.println(rootAsMap);
Map query = (Map) rootAsMap.get("query");
Map address = (Map) rootAsMap.get("address");
Map addressProperties = (Map) address.get("properties");
String county = (String) addressProperties.get("county");
System.out.println("County is " + county);
}
}
Now, this whole Map juggling also illustrates Bozho's point pretty well, using full binding (by creating a Java class that reflects the content of the JSON data) will work better in the end.
The two best options that I know of are:
Jackson
gson
Using them is a matter of calling one method of the mapper. But remember that since Java is statically-typed, you may have to create an object that has the required structure. (You don't have to, but it feels more natural)
From http://www.json.org, under the Java section:
http://www.json.org/java/index.html
http://json-lib.sourceforge.net/
http://code.google.com/p/json-simple/
http://code.google.com/p/jjson/
Pick your poison
With Jackson, following is the approach I'd take. Since the coordinates in the JSON come in two different formats -- sometimes an object, sometimes an array -- the solution is mildly complicated with necessary custom deserialization processing.
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.ObjectCodec;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.node.ArrayNode;
public class JacksonFoo
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibilityChecker(mapper.getVisibilityChecker().withFieldVisibility(Visibility.ANY));
mapper.registerModule(
new SimpleModule("CoordinatesDeserializer", Version.unknownVersion())
.addDeserializer(Coordinates.class, new CoordinatesDeserializer()));
Result result = mapper.readValue(new File("input.json"), Result.class);
System.out.println(mapper.writeValueAsString(result));
}
}
class CoordinatesDeserializer extends JsonDeserializer<Coordinates>
{
#Override
public Coordinates deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
if (node.isObject())
{
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibilityChecker(mapper.getVisibilityChecker().withFieldVisibility(Visibility.ANY));
return mapper.readValue(node, Coordinates.class);
}
// else it's an array
ArrayNode array = (ArrayNode) node;
Coordinates coordinates = new Coordinates();
coordinates.latitude = codec.treeToValue(array.get(0), BigDecimal.class);
coordinates.latitude = codec.treeToValue(array.get(1), BigDecimal.class);
return coordinates;
}
}
class Result
{
Coordinates query;
BigDecimal timestamp;
Address address;
}
class Coordinates
{
BigDecimal latitude;
BigDecimal longitude;
}
class Address
{
String type;
Geometry geometry;
AddressDetails properties;
}
class Geometry
{
String type;
Coordinates coordinates;
}
class AddressDetails
{
String address;
BigDecimal distance;
String postcode;
String city;
String county;
String province;
String country;
}