How to Parse Dynamic JSON Fields with Gson

How to parse dynamic JSON fields with GSON?

According to GSON documentation you can do things like:

Type mapType = new TypeToken<Map<Integer, Result> >() {}.getType(); // define generic type
Map<Integer, Result> result= gson.fromJson(new InputStreamReader(source), mapType);

Or you can try to write custom serializer for your class.

Disclaimer: I too, have no experience with GSon but with other frameworks like Jackson.

Parse Dynamic Json Objects in gson

To start, your JSON causes an exception to be thrown because it is invalid - You have an extra comma at the end of the last value in the second example. "3": 8536, should be "3": 8536.

After fixing that, this should be a simple task provided you define your objects correctly. Here is what I came up with:

public class Results {
@SerializedName("Result")
List<Result> results;
}

public class Result {
String title;
Map<String, Integer> results;
}

From there, the Result class needs to be deserialized in a special fashion, since the fields in the Result class do not directly map to entries in the JSON. Instead, we need to pull off the first and second elements of the JsonArray that is contained within each Result, and parse it accordingly. That looks like this:

public class ResultDeserializer implements JsonDeserializer<Result> {

@Override
public Result deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray array = json.getAsJsonArray();

Result result = new Result();
result.title = array.get(0).getAsString();

result.results = new LinkedHashMap<String, Integer>();
for(Entry<String, JsonElement> entry : array.get(1).getAsJsonObject().entrySet()) {
result.results.put(entry.getKey(), entry.getValue().getAsInt());
}

return result;
}
}

Note that my example omits error checking. Finally, register this deserializer, and you should be all set:

Gson gson = new GsonBuilder().registerTypeAdapter(Result.class, new ResultDeserializer()).create();

Results results = gson.fromJson(json, Results.class);

for(Result r : results.results) {
System.out.println("Title = " + r.title);
for(Entry<String, Integer> entry : r.results.entrySet()) {
System.out.println("\t " + entry.getKey() + " -> " + entry.getValue());
}
}

This prints:

Title = Title
0 -> 1323
1 -> 3358
2 -> 2123
3 -> 8536
4 -> 1399
5 -> 9303
7 -> 9732
8 -> 3433
9 -> 1383
Title = Title
0 -> 1323
1 -> 3358
2 -> 2123
3 -> 8536

I'll leave it to the OP to implement the reverse, that is a serializer for Result to produce the same results.

GSON parsing dynamic JSON field

I tried it in this form:

The Json

{
"id": 6,
"error": "0",
"dates": {
"34234" : "2011-01-01"
"87474" : "2011-08-09"
"74857" : "2011-09-22"
}
}

And the Response.java

public class Response {
public Integer id;
public String error;
public Map<Integer, String> dates;
}

At least that seemed to work out of the box.

Using the Gson library to parse unknown data dynamically

You can simply use java.util.Map that is an associative key/value container where keys and values are arbitrary objects, and can align with JSON dynamic objects using Gson really straight-forward. You just have to define appropriate mappings (I made the field collapsed to save some visual space):

final class Response {
@SerializedName("schools") final HomeSchool school = null;
@SerializedName("school") final Map<Integer, School> schools = null;
}

final class HomeSchool {
@SerializedName("home") final int home = Integer.valueOf(0);
@SerializedName("statelevel") final int stateLevel = Integer.valueOf(0);
}

final class School {
@SerializedName("schoolname") final String name = null;
@SerializedName("nameshort") final String shortName = null;
@SerializedName("students") final Map<Integer, Student> students = null;
}

final class Student {
@SerializedName("name") final String name = null;
@SerializedName("isCR") final boolean isCr = Boolean.valueOf(false);
@SerializedName("maths") final Maths maths = null;
}

final class Maths {
@SerializedName("score") final int score = Integer.valueOf(0);
@SerializedName("lastscore") final int lastScore = Integer.valueOf(0);
}

Now, once you have the mappings, you can easily deserialize your JSON:

private static final Gson gson = new Gson();

public static void main(final String... args) {
final Response response = gson.fromJson(JSON, Response.class);
for ( final Entry<Integer, School> schoolEntry : response.schools.entrySet() ) {
final School school = schoolEntry.getValue();
System.out.println(schoolEntry.getKey() + " " + school.name);
for ( final Entry<Integer, Student> studentEntry : school.students.entrySet() ) {
final Student student = studentEntry.getValue();
System.out.println("\t" + studentEntry.getKey()
+ " " + student.name
+ " CR:" + (student.isCr ? "+" : "-")
+ " (" + student.maths.score + ", " + student.maths.lastScore + ")"
);
}
}
}

1103 Indira School

2201 Ritesh CR:+ (95, 86)

2202 Sanket CR:- (98, 90)

2203 Ajit CR:- (94, 87)

1348 Patil School

3201 Ravi CR:- (95, 86)

3202 Raj CR:+ (98, 90)

3203 Ram CR:- (94, 87)

The type token suggestions are partially correct: they are used to deserialize the objects you cannot or don't have concrete mappings for, like lists of something, or maps of strings to something. In your case Gson simply analyzes the field declarations to resolve the map types (both keys and values).

Parse json containing dynamic fields using Gson

You could try these options:

  • Gson 2.5 supports multiple field names for the same variable. If the value is determined in runtime but the set of values is limited then you could do something like this:

    @SerializedName(value="name1", alternate={"name2", "name3"}) String b;
  • Create a custom deserializer:

    public static class YourModelDeserializer implements JsonDeserializer<YourModel> {
    @Override
    public YourModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
    throws JsonParseException
    {
    /* Deserialize here */;
    }
    }

Parsing object with dynamic key through gson

Maybe you can try this:
Episode class

    public class Episode {

@SerializedName("season")
@Expose
private Integer season;
@SerializedName("name")
@Expose
private String name;
@SerializedName("number")
@Expose
private Integer number;

public Integer getSeason() {
return season;
}

public void setSeason(Integer season) {
this.season = season;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getNumber() {
return number;
}

public void setNumber(Integer number) {
this.number = number;
}

}

Then Serial class:

    public class Serial {

@SerializedName("episodes")
@Expose
private List<Episode> episodes = new ArrayList<Episode>();
@SerializedName("year")
@Expose
private Integer year;

public List<Episode> getEpisodes() {
return episodes;
}

public void setEpisodes(List<Episode> episodes) {
this.episodes = episodes;
}

public Integer getYear() {
return year;
}

public void setYear(Integer year) {
this.year = year;
}

}

Then to generate JAVA objects with dynamic keys from JSON just run this:

Type type = new TypeToken<Map<String, Serial>>() {
}.getType();
Map<String, Serial> result = new Gson()
.fromJson(new InputStreamReader(new ByteArrayInputStream(jsonString.getBytes())), type);

To get for example Narcos serial information you need to write this:

Serial narcosSerial = result.get("Narcos");

Parse JSON with dynamic key object inside dynamic key object with Gson

Your models cannot map your JSON just because Gson default configuration clearly gets them unmatched.

You can have two "default" ways:

static

... since you didn't really mention why your JSON is considered dynamic:

final class XYZ {

final ABC x = null;
final ABC y = null;
final ABC z = null;

}
final class ABC {

final OneTwo a = null;
final OneTwo b = null;

}
final class OneTwo {

@SerializedName("1")
final List<Integer> one = null;

@SerializedName("2")
final List<Integer> two = null;

}

Example:

try ( final Reader reader = getPackageResourceReader(Q43695739.class, "dynamic.json") ) {
final XYZ xyz = gson.fromJson(reader, XYZ.class);
System.out.println(xyz.x.b.two);
}

dynamic (by deserialization)

... assuming your keys are dynamic, but the structure remains the same:

private static final Type stringToStringToStringToIntegerListType = new TypeToken<Map<String, Map<String, Map<String, List<Integer>>>>>() {
}.getType();
try ( final Reader reader = getPackageResourceReader(Q43695739.class, "dynamic.json") ) {
final Map<String, Map<String, Map<String, List<Integer>>>> m = gson.fromJson(reader, stringToStringToStringToIntegerListType);
System.out.println(m.get("x").get("b").get("2"));
}

dynamic (by JSON trees)

Another true dynamic approach that may be helpful for some scenarios. Also note that JSONObject is not in the Gson realm: you probably might have imported this one from the org.json package. Gson uses camel-cased names like JsonElement, JsonObject, etc.

try ( final Reader reader = getPackageResourceReader(Q43695739.class, "dynamic.json") ) {
final JsonElement jsonElement = gson.fromJson(reader, JsonElement.class)
.getAsJsonObject()
.getAsJsonObject("x")
.getAsJsonObject("b")
.getAsJsonArray("2");
System.out.println(jsonElement);
}

The first and the second examples produce java.util.List instances

[1, 2, 3, 4]

The third example returns a JsonArray instance with a slightly different toString implementation:

[1,2,3,4]

parsing json to java using GSON where json have dynamic objects inside another object

You can do it this way. You can use Map, since your keys and values both are dynamic.

public class Wrapper {
public Map<String, Map<String, String>> properties;
}

You can even get rid of the wrapper class and use Map<String, Map<String, String>> directly

Gson: Dynamic field parsing based on another field

Maybe you want to use GSON's RuntimeTypeAdapterFactory. It allows you to determine the type of class to be deserialized automatically from type field: conveniently named as type by default.

To have inheritance right for GSON to deserialize I suggest small changes to your POJOs. Create to class for Request like:

@Getter @Setter
public class Request {
private String type;
@Getter @Setter
public static class Content {
String password;
}
}

Override it and Content per request type, like:

@Getter @Setter
public class ForwardRequest extends Request {
@Getter @Setter
public static class ForwardContent extends Content {
private String deviceId;
}
private ForwardContent content;
}

and

@Getter @Setter
public class LoginRequest extends Request {
@Getter @Setter
public static class LoginContent extends Content {
private String username;
}
private LoginContent content;
}

With classes like above it is just:

@Test
public void test() {
// Tell the top class
RuntimeTypeAdapterFactory<Request> rttaf = RuntimeTypeAdapterFactory.of(Request.class)
// Register inheriting types and the values per type field
.registerSubtype(ForwardRequest.class, "Forward")
.registerSubtype(LoginRequest.class, "Login");
Gson gson = new GsonBuilder().setPrettyPrinting()
.registerTypeAdapterFactory(rttaf)
.create();
// Constructed an array of your two types to be deserialized at the same time
String jsonArr = "["
+ "{\"type\": \"Login\", \"content\": {\"username\": \"a\", \"password\": \"b\"}}"
+ ","
+ "{\"type\": \"Forward\", \"content\": {\"deviceId\": \"a\", \"password\": \"b\"}}"
+ "]";
// Deserialize the array as Request[]
Request[] requests = gson.fromJson(jsonArr, Request[].class);
log.info("{}", requests[0].getClass());
log.info("{}", requests[1].getClass());
}

Outpur for the above would be similar to:

class org.example.gson.LoginRequest

class org.example.gson.ForwardRequest

You just need to copy the file provided in the link at the top of answer to include RuntimeTypeAdapterFactory into your project.

Update: About deserializing type or any other field that contains the information about type: GSON leaves it out on purpose. It is a kind of a transient field is it not? You need to know the value of type before you can deserialize so GSON does not bother to deserialize it anymore.

As an another example - to clarify this a bit - if you change the test string like:

String jsonArr = "["
+ "{\"type\": \"LoginRequest\", \"content\": {\"username\": \"a\", \"password\": \"b\"}}"
+ ","
+ "{\"type\": \"ForwardRequest\", \"content\": {\"deviceId\": \"a\", \"password\": \"b\"}}"
+ "]";

so that type holds simple names of classes (which is usually the
case) you can register subtypes just like:

.registerSubtype(ForwardRequest.class)
.registerSubtype(LoginRequest.class);

and GSON would expect class simple name as a value for JSON's type attribute. Why would you hold class name in separate field because it is gettable Class.getSimpleName()?

However you of course might sometimes need to serialize it to other clients.



Related Topics



Leave a reply



Submit