Java Read JSON attribute names without parsing to class - java

I have a problem.
Earlier, I parsed multiple objects from 1 single JSON to an ArrayList<Object>. This worked great, but now I need to do something different and I have no idea how to this. I have the following JSON:
{
"Market":"USDT",
"Coin":"BTC",
"Candles":{
"USDT":{
"BTC":{
"3h":[
{
"Close":"used"
}
],
"1h":[
{
"EMA20":"used",
"EMA200":"used"
}
],
"5m":[
{
"EMA5":"used",
"EMA20":"used"
},
{
"EMA5":"used",
"EMA20":"used"
}
]
}
}
}
}
From this JSON I want to get an ArrayList<String> with only the values: "3h", "1h", "5m".
The values inside the array don't matter for me, I just need those 3 periods. Here is the json parser I used for parsing something to a class:
public ArrayList<Agent> parseJsonToList(String StrJSON) {
Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, (JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) -> {
try{
return LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (DateTimeParseException e){
return LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"));
}
}).create();
Type listType = new TypeToken<ArrayList<Agent>>() {}.getType();
return gson.fromJson(StrJSON, listType);
}
Now how can I get those 3 values from my new JSON, without creating Classes (if possible)?

You can use this ugly code for your purposes, but this code is just to show how you can achieve that and I don't recommend use this code in production:
private List<String> extractPeriods(String jsonString) {
Gson gson = new Gson();
Map map = gson.fromJson(jsonString, Map.class);
Map candlesMap = (Map) map.get("Candles");
if (Objects.isNull(candlesMap))
return null;
Map usdtMap = (Map) candlesMap.get("USDT");
if (Objects.isNull(usdtMap))
return null;
Map btcMap = (Map) usdtMap.get("BTC");
if (Objects.isNull(btcMap))
return null;
return new ArrayList<>(btcMap.keySet());
}
You can also write regexp to search all this values, of course.
Or you can create objects only partially. For example you can create objects for root, Candles, USDT, and for BTC field just use a Map type.

Related

Deserialize a JSON array into a Java object (Not a List nor a Collection) using Jackson

I was previously using Gson and decided to move to Jackson for practical reasons.
Previously, I had a JSON deserializer that was deserializing an array of json objects into a single java object.
The json structure of the field is :
{
id: number,
type: (0 | 1),
allow: number,
deny: number
}[]
And I need to deserialize this array into my own object: PermissionOverwrites.
Previously, I was using this code with Gson:
JsonArray array = json.getAsJsonArray();
Map<Permissionable, Permissions> map = new HashMap<>();
for (JsonElement element : array) {
JsonObject object = element.getAsJsonObject();
Permissionable p = switch (object.get("type").getAsInt()) {
case 0 -> new Permissionable.MockPermissionable(object.get("id").getAsLong());
case 1 -> new Permissionable.MockPermissionable(object.get("id").getAsLong());
default -> throw new JsonParseException("Unknown permissionable type");
};
Permissions perms = new Permissions(object.get("allow").getAsLong(), object.get("deny").getAsLong());
map.put(p, perms);
}
return new PermissionOverwrites(map);
But now, with Jackson, I've got a JsonParser and a DeserializationContext and I can't find anything in any of the two files to parse an ArrayNode. Do you know any way I can do this using Jackson.
Thank you in advance!
EDIT:
PermissionsOverwrites.java
#ToString
#EqualsAndHashCode
public class PermissionOverwrites {
private Map<Permissionable, Permissions> overwrites;
public PermissionOverwrites(Map<Permissionable, Permissions> overwrites) {
this.overwrites = overwrites;
}
public PermissionOverwrites() {
this.overwrites = new HashMap<>();
}
public Map<Permissionable, Permissions> getOverwrites() {
return Collections.unmodifiableMap(overwrites);
}
}
Example of the JSON object:
[
{
"id": "111111111111111111",
"type": "0",
"allow": "01010",
"deny": "10101"
}
],
This code uses Jackson to do the same as your code that uses Gson:
JsonNode array = mapper.readTree(json);
for (JsonNode element : array) {
Map<Permissionable, Permissions> map = new HashMap<>();
Permissionable p = switch (element.get("type").asInt()) {
case 0 -> new Permissionable.MockPermissionable(element.get("id").asLong());
case 1 -> new Permissionable.MockPermissionable(element.get("id").asLong());
default -> throw new JsonParseException("Unknown permissionable type", null);
};
Permissions perms = new Permissions(element.get("allow").asLong(), element.get("deny").asLong());
map.put(p, perms);
}
return new PermissionOverwrites(map);
Here's an article about how to work with Jackson JSON tree model .

Java Gson parse Json object to array

I'm trying to parse the below Json using the Gson lib in Java. When using other languages, such as C#, this JSON is parsed into an array, however it seems Gson converts this into a set of java attributes (which to be honest, makes more sense to me). Does anyone know if I can change this behaviour of the Gson lib?
{
"Outer": {
"0": {
"Attr1": 12345,
"Attr2": 67890
},
"1": {
"Attr1": 54321,
"Attr2": 09876
}
}
}
The below code demonstrates how Gson parses the array as a JsonObject. To be clear, I realise I've referenced outer as a JsonObject but I was just doing this to demonstrate the code. If I try and reference outer as an JsonArray, the code fails.
String json = "{\"Outer\": { \"0\": { \"Attr1\": 12345, \"Attr2\": 67890 }, \"1\": { \"Attr1\": 54321, \"Attr2\": 09876 }}}";
Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.setLenient()
.serializeNulls()
.create();
JsonObject jo = gson.fromJson(json, JsonObject.class);
JsonObject outer = jo.getAsJsonObject("Outer");
System.out.println(outer);
System.out.println(outer.isJsonArray());
Result:
{"0":{"Attr1":12345,"Attr2":67890},"1":{"Attr1":54321,"Attr2":"09876"}}
false
//edit
I'm using this current simple Json as an example, however my application of this code will be to parse Json that's of varying and unknown shape. I therefore need Gson to automatically parse this to an array so that the isJsonArray returns true.
TL;DR: See "Using Deserializer" section at the bottom for parsing straight to array.
That JSON does not contain any arrays. An array would use the [...] JSON syntax.
Normally, a JSON object would map to a POJO, with the name in the name/value pairs mapping to a field of the POJO.
However, a JSON object can also be mapped to a Map, which is especially useful when the names are dynamic, since POJO fields are static.
Using Map
The JSON object with numeric values as names can be mapped to a Map<Integer, ?>, e.g. to parse that JSON to POJOs, do it like this:
class Root {
#SerializedName("Outer")
public Map<Integer, Outer> outer;
#Override
public String toString() {
return "Root[outer=" + this.outer + "]";
}
}
class Outer {
#SerializedName("Attr1")
public int attr1;
#SerializedName("Attr2")
public int attr2;
#Override
public String toString() {
return "Outer[attr1=" + this.attr1 + ", attr2=" + this.attr2 + "]";
}
}
Test
Gson gson = new GsonBuilder().create();
Root root;
try (BufferedReader in = Files.newBufferedReader(Paths.get("test.json"))) {
root = gson.fromJson(in, Root.class);
}
System.out.println(root);
Output
Root[outer={0=Outer[attr1=12345, attr2=67890], 1=Outer[attr1=54321, attr2=9876]}]
Get as Array
You can then add a helper method to the Root class to get that as an array:
public Outer[] getOuterAsArray() {
if (this.outer == null)
return null;
if (this.outer.isEmpty())
return new Outer[0];
int maxKey = this.outer.keySet().stream().mapToInt(Integer::intValue).max().getAsInt();
Outer[] arr = new Outer[maxKey + 1];
this.outer.forEach((k, v) -> arr[k] = v);
return arr;
}
Test
System.out.println(Arrays.toString(root.getOuterAsArray()));
Output
[Outer[attr1=12345, attr2=67890], Outer[attr1=54321, attr2=9876]]
Using Deserializer
However, it would likely be more useful if the conversion to array is done while parsing, so you need to write a JsonDeserializer and tell Gson about it using #JsonAdapter:
class Root {
#SerializedName("Outer")
#JsonAdapter(OuterArrayDeserializer.class)
public Outer[] outer;
#Override
public String toString() {
return "Root[outer=" + Arrays.toString(this.outer) + "]";
}
}
class OuterArrayDeserializer implements JsonDeserializer<Outer[]> {
#Override
public Outer[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Parse JSON array normally
if (json.isJsonArray())
return context.deserialize(json, Outer[].class);
// Parse JSON object using names as array indexes
JsonObject obj = json.getAsJsonObject();
if (obj.size() == 0)
return new Outer[0];
int maxKey = obj.keySet().stream().mapToInt(Integer::parseInt).max().getAsInt();
Outer[] arr = new Outer[maxKey + 1];
for (Entry<String, JsonElement> e : obj.entrySet())
arr[Integer.parseInt(e.getKey())] = context.deserialize(e.getValue(), Outer.class);
return arr;
}
}
Same Outer class and test code as above.
Output
Root[outer=[Outer[attr1=12345, attr2=67890], Outer[attr1=54321, attr2=9876]]]
I'll asume your JsonObject is a POJO class such like:
public Inner[] outer;
If you want an array of objects you can change your code to:
Inner[] jo = gson.fromJson(json, Inner[].class);
Jackson – Marshall String to JsonNode will be useful in your case.with following pom:-
//POM FILE
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
//JAVA CODE
//read json file data to String
byte[] jsonData = Files.readAllBytes(Paths.get("employee.txt"));
//create ObjectMapper instance
ObjectMapper objectMapper = new ObjectMapper();
//read JSON like DOM Parser
JsonNode rootNode = objectMapper.readTree(jsonData);
JsonNode idNode = rootNode.path("id");
System.out.println("id = "+idNode.asInt());
JsonNode phoneNosNode = rootNode.path("phoneNumbers");
Iterator<JsonNode> elements = phoneNosNode.elements();
while(elements.hasNext()){
JsonNode phone = elements.next();
System.out.println("Phone No = "+phone.asLong());
}
You can use the JsonNode class's method findParent findValue and findPath which reduce your code as compare to another parsing library.
Please refer below code
1.To get an array of Objects (outerArray)
2.You can extract a JsonArray (outerJsonArray) containing values of inner objects in Outer (in case keys aren't significant for further use)
String json = "{\"Outer\": { \"0\": { \"Attr1\": 12345, \"Attr2\": 67890 }, \"1\": { \"Attr1\": 54321, \"Attr2\": 09876 }}}";
Gson gson = new GsonBuilder().disableHtmlEscaping().setLenient().serializeNulls().create();
JsonObject jo = gson.fromJson(json, JsonObject.class);
JsonObject outer = jo.getAsJsonObject("Outer");
Object[] outerArray = outer.entrySet().toArray();
// outerArray: [0={"Attr1":12345,"Attr2":67890}, 1={"Attr1":54321,"Attr2":"09876"}]
JsonArray outerJsonArray = new JsonArray();
outer.keySet().stream().forEach(key -> {
outerJsonArray.add(outer.get(key));
});
//jsonArray=[{"Attr1":12345,"Attr2":67890},{"Attr1":54321,"Attr2":"09876"}]
System.out.println(outer);
System.out.println(outerJsonArray.isJsonArray() + " " + outerJsonArray);

How to convert List to a JSON Object using GSON?

I have a List which I need to convert into JSON Object using GSON. My JSON Object has JSON Array in it.
public class DataResponse {
private List<ClientResponse> apps;
// getters and setters
public static class ClientResponse {
private double mean;
private double deviation;
private int code;
private String pack;
private int version;
// getters and setters
}
}
Below is my code in which I need to convert my List to JSON Object which has JSON Array in it -
public void marshal(Object response) {
List<DataResponse.ClientResponse> clientResponse = ((DataResponse) response).getClientResponse();
// now how do I convert clientResponse list to JSON Object which has JSON Array in it using GSON?
// String jsonObject = ??
}
As of now, I only have two items in List - So I need my JSON Object like this -
{
"apps":[
{
"mean":1.2,
"deviation":1.3
"code":100,
"pack":"hello",
"version":1
},
{
"mean":1.5,
"deviation":1.1
"code":200,
"pack":"world",
"version":2
}
]
}
What is the best way to do this?
There is a sample from google gson documentation on how to actually convert the list to json string:
Type listType = new TypeToken<List<String>>() {}.getType();
List<String> target = new LinkedList<String>();
target.add("blah");
Gson gson = new Gson();
String json = gson.toJson(target, listType);
List<String> target2 = gson.fromJson(json, listType);
You need to set the type of list in toJson method and pass the list object to convert it to json string or vice versa.
If response in your marshal method is a DataResponse, then that's what you should be serializing.
Gson gson = new Gson();
gson.toJson(response);
That will give you the JSON output you are looking for.
Assuming you also want to get json in format
{
"apps": [
{
"mean": 1.2,
"deviation": 1.3,
"code": 100,
"pack": "hello",
"version": 1
},
{
"mean": 1.5,
"deviation": 1.1,
"code": 200,
"pack": "world",
"version": 2
}
]
}
instead of
{"apps":[{"mean":1.2,"deviation":1.3,"code":100,"pack":"hello","version":1},{"mean":1.5,"deviation":1.1,"code":200,"pack":"world","version":2}]}
you can use pretty printing. To do so use
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(dataResponse);
Make sure to convert your collection to Array:
Gson().toJson(objectsList.toTypedArray(), Array<CustomObject>::class.java)
We can also use another workaround by first creating an array of myObject then convert them into list.
final Optional<List<MyObject>> sortInput = Optional.ofNullable(jsonArgument)
.map(jsonArgument -> GSON.toJson(jsonArgument, ArrayList.class))
.map(gson -> GSON.fromJson(gson, MyObject[].class))
.map(myObjectArray -> Arrays.asList(myObjectArray));
Benifits:
we are not using reflection here. :)

Make Gson throw exception on parsing JSON with duplicated key

I'm parsing simple JSON object with Gson. I want it to throw some error when key name is duplicated. E.g.
{
a: 2,
a: 3
}
In my case, Gson parses such JSON and sets a to 3. I want it to throw some exception.
I know I can parse JSON as map, and then Gson throws exception in such case, but only if the duplicated key is not nested in the map. If I have e.g. JSON like this:
{
a: 2,
b: {
dup: 1,
dup: 2
}
}
Still, it is parsed without any exception and I have only one "dup" with value 2.
Can I somehow setup Gson to throw error in such case? Or to have duplicated entries in JsonObject instance, so that I can detect it myself (but I doubt that, as it would be invalid JsonObject)
Reproducible example
String json = "{\"a\":2, \"a\":3}";
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
System.out.println(jsonObject);
prints out
{"a":3}
1) You may edit the source of gson a little bit. This is just a suggestion to understand how things work. I don't advice you to use this on a real/production environment.
Gson uses com.google.gson.internal.LinkedTreeMap while parsing a json string to a JsonObject. For testing issues you can copy that class into your project with the same name and package name. And edit its put method to not allow duplicate keys.
#Override
public V put(K key, V value) {
if (key == null) {
throw new NullPointerException("key == null");
}
// my edit here
if(find(key, false) != null) {
throw new IllegalArgumentException("'" + key.toString() + "' is duplicate key for json!");
}
Node<K, V> created = find(key, true);
V result = created.value;
created.value = value;
return result;
}
2) Another clean solution is to define custom classes which are going to map to your json strings. Then write their custom TypeAdapters
3) Do it by using a Deserializer? I don't think it is possible. If you try to use it you'll see that you already have a jsonObject there which your duplicate keys are handled as one.
You can try this way:
String json = "{\"a\":2, \"a\":3}";
Gson gson = new Gson();
Type mapType = new TypeToken<Map<String, String>>() {}.getType();
Map<String, String> map = gson.fromJson(json, mapType);
And if json is more complex than JsonObject can be used as map value type:
Type mapType = new TypeToken<Map<String, JsonObject>>() {}.getType();

How to prevent Gson from converting a long number (a json string ) to scientific notation format?

I need to convert json string to java object and display it as a long. The json string is a fixed array of long numbers:
{numbers
[ 268627104, 485677888, 506884800 ] }
The code to convert works fine in all cases except for numbers ending in 0. It converts those to a scientific notation number format:
public static Object fromJson(HttpResponse response, Class<?> classOf)
throws IOException {
InputStream instream = response.getResponseInputStream();
Object obj = null;
try {
Reader reader = new InputStreamReader(instream, HTTP.UTF_8);
Gson gson = new Gson();
obj = gson.fromJson(reader, classOf);
Logger.d(TAG, "json --> "+gson.toJson(obj));
} catch (UnsupportedEncodingException e) {
Logger.e(TAG, "unsupported encoding", e);
} catch (Exception e) {
Logger.e(TAG, "json parsing error", e);
}
return obj;
}
The actual result:
Java object : 268627104, 485677888, 5.068848E+8
Notice the last number is converted to a scientific notation format. Can anyone suggest what could be done to work around it or prevent it or undo it? I'm using Gson v1.7.1
If serializing to a String is an option for you, you can configure GSON to do so with:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setLongSerializationPolicy( LongSerializationPolicy.STRING );
Gson gson = gsonBuilder.create();
This will produce something like:
{numbers : [ "268627104", "485677888", "506884800" ] }
Another work around is to use the JsonParser class instead. This will return the Gson object representations (JsonElement) rather than a user defined class, but avoids the problem of conversion to scientific notation.
import java.lang.reflect.Type;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class GsonTest
{
public static void main(String[] args)
{
String json = "{numbers:[268627104,485677888,506884800]}";
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> jsonMap = gson.fromJson(json, type);
System.out.println("Gson output:");
System.out.println(jsonMap);
JsonParser jsonParser = new JsonParser();
JsonElement jsonElement = jsonParser.parse(json);
System.out.println("JsonParser output:");
System.out.println(jsonElement);
}
}
Code Output:
Gson output:
{numbers=[2.68627104E8, 4.85677888E8, 5.068848E8]}
JsonParser output:
{"numbers":[268627104,485677888,506884800]}
I had a similar problem, and it not only converts integers to double, but it actually loses precision for certain long numbers, as described in this related question.
I tracked down this conversion to ObjectTypeAdapter's read method, specifically:
case NUMBER:
return in.nextDouble();
It may be possible to plug in a modified TypeAdapter for Object, but I couldn't get that to work, so instead I just copied the read method (Object read(JsonReader in)) to my own code and modified the above lines to this:
case NUMBER:
final String s = in.nextString();
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
// ignore
}
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
// ignore
}
return Double.parseDouble(s);
I wish Gson did this by default..
Then I put the other connecting pieces in a helper method that looks something like this:
public static Object parse(final Reader r) {
try (final JsonReader jr = new JsonReader(r)) {
jr.setLenient(true);
boolean empty = true;
Object o = null;
try {
jr.peek();
empty = false;
o = read(jr);
} catch (EOFException e) {
if (!empty) {
throw new JsonSyntaxException(e);
}
}
if (o != null && jr.peek() != JsonToken.END_DOCUMENT) {
throw new JsonIOException("JSON document was not fully consumed.");
}
return o;
} catch (IOException e) {
throw new JsonIOException(e);
}
}
So now instead of new Gson().fromJson(r, Object.class), I call parse(r).
This works well for me because I want to be able to parse json data with any structure, but if you have a particular class you're targeting, you probably just need to eliminate occurrences of Object within that class's members.
Got the same issue, after some investigation here is what I found.
The behavior:
Gson
For a number without fractional part, Gson would convert it as Double,
Jackson
For a number without fractional part, Jackson would convert it as Integer or Long, depends on how large the number is.
Possible solutions:
Convert Gson's return value from Double to Long, explicitly.
Use Jackson instead.
I prefer this.
Code - test for Jackson
ParseNumberTest.java:
import java.util.List;
import org.testng.Assert;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* test - jackson parse numbers,
*
* #author eric
* #date Jan 13, 2018 12:28:36 AM
*/
public class ParseNumberTest {
#Test
public void test() throws Exception {
String jsonFn = "numbers.json";
ObjectMapper mapper = new ObjectMapper();
DummyData dd = mapper.readValue(this.getClass().getResourceAsStream(jsonFn), DummyData.class);
for (Object data : dd.dataList) {
System.out.printf("data type: %s, value: %s\n", data.getClass().getName(), data.toString());
Assert.assertTrue(data.getClass() == Double.class || data.getClass() == Long.class || data.getClass() == Integer.class);
System.out.printf("%s\n\n", "------------");
}
}
static class DummyData {
List<Object> dataList;
public List<Object> getDataList() {
return dataList;
}
public void setDataList(List<Object> dataList) {
this.dataList = dataList;
}
}
}
numbers.json:
{
"dataList": [
150000000000,
150778742934,
150000,
150000.0
]
}
How to run:
The test case is based on Jackson & TestNG.
Put numbers.json at the same package as ParseNumberTest.java.
Run as testng test, then it would print type & value of the parse result.
Output:
data type: java.lang.Long, value: 150000000000
------------
data type: java.lang.Long, value: 150778742934
------------
data type: java.lang.Integer, value: 150000
------------
data type: java.lang.Double, value: 150000.0
------------
PASSED: test
Not smart, but still working method is to add " at the start and at the end of the number. Then after processing is finished, delete it.
We can use the below code solution for number Long:
Document doc = documentCursor.next();
JsonWriterSettings relaxed = JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build();
CustomeObject obj = gson.fromJson(doc.toJson(relaxed), CustomeObject.class);
The best solution in case you need them as String was to force attributes to be quoted with a single quote before doing the conversion.
Do changes like this:
String readerAsString = convertReaderToString(reader);
readerAsString = readerAsString.toString().replace("=", "='");
readerAsString = readerAsString.toString().replace(",", "',");
readerAsString = readerAsString.toString().replace("}]", "'}]");
data class Answer(
val question: String,
val value: Any
)
Given the value:Any property above, I find it dubious that Gson encounters a JSON value of 1 (not "1") and cannot INFER the blatantly obvious truth: 1 is an Int. Instead, Gson converts the integer value to the double 1.0.
Gson should only convert a JSON value from 1 to 1.0 if the value property above was of type Float or Double. When Gson encounters a property whose type is Any, it should (quite simply) infer the type from the JSON value it receives. Unfortunately, it doesn't, preferring to actually corrupt incoming integer values by casting them to Double, which unsurprisingly immediately causes exceptions.
I can find no reasonable solution to this peculiarity of the Gson parser. As such, I'm forced to either manually convert all those double values back into int values after using Gson or implement my own generic custom type adapter for Gson. Neither of these options is at all appealing.
I did not find a solution to my problem of gson formatting numbers ending in 0 to scientific notation. I instead used a work-around to convert this scientific notation into a double that I formatted with commas. "value" is the json string.
private String formatNumber(String value) {
double dValue = Double.parseDouble(value);
String pattern = "#,###";
DecimalFormat formatter = new DecimalFormat(pattern);
String newNumber = formatter.format(dValue);
return newNumber;
}
This doesn't answer the question asked but is an added step to work-around the problem to display the numbers as required by the system.

Categories

Resources