I need to get data from a badly designed web API which returns the list of objects in the form of JSON object:
{
"29593": { ..object to parse },
"29594": { ..object to parse },
"29600": { ..object to parse }
}
I need to create POJO for this response, but the issue is that these integers are changing, they are like object IDs. I don't know how to extract these integers from the JSON keys and then use the inner JSON objects further in another POJO class (I know basic Gson mapping when the key has a fixed value).
Is it even possible?
The solution is to use a custom JsonDeserializer from gson library, here is a example:
public class MyAwesomeDeserializer implements JsonDeserializer<MyModel> {
public MyModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject eJson = json.getAsJsonObject();
Set<String> keys = eJson.keySet();
MyModel myModel = new MyModel();
for (String key: keys) {
JsonObject asJsonObject = eJson.get(key).getAsJsonObject();
ItemOfMyModel itemOfMyModel = context.deserialize(asJsonObject, ItemOfMyModel.class);
myModel.addItemOfMyModel(itemOfMyModel);
}
return myModel;
}
}
and dont forget to add your custom deserializer as a type adapter to gson builder:
Gson gson = new GsonBuilder()
.registerTypeAdapter(MyModel.class, new MyAwesomeDeserializer())
.create()
Related
I currently have a custom serialiser. Purpose of my custom serialiser is to format my JSON object from this: {"id":["21","22", 23"]} to this {"id":"21,22,23"}.
My current implementation of my custom serialiser:
public static class ListSerialiser implements JsonSerializer<List<Member>> {
#Override
public JsonElement serialize(List<Member> src, Type type, JsonSerializationContext context) {
JsonObject object = new JsonObject();
List<String> memberId = new ArrayList<>(src.size());
for (Member member : src) {
memberId.add("" + Member.getId());
}
String memberIdAsString = TextUtils.join(",", memberId);
object.addProperty("[%s]", memberIdAsString);
return object;
}
}
Although my implementation got me what I wanted, just out of curiosity, I was wondering if there's a way to serialise without having to use Text Utils or string formatters to achieve this outcome: {"id":"21,22,23"}
There are a lot of ways to join strings, you can see most of them here
My favourite is:
String memberIdAsString = memberId.stream().collect(Collectors.joining(","));
Is it any way to save ArrayList to sharedpreferences? Thank you
ArrayList<Class> activityList = new ArrayList<>();
activityList.add(Level1Activity.class);
activityList.add(Level2Activity.class);
activityList.add(Level3Activity.class);
activityList.add(Level4Activity.class);
activityList.add(Level5Activity.class);
I already answered this to your other question but just in case, I'll re-write it here and explain it more a bit.
You can use Gson to convert your list into a Json String so that you can save it in SharedPreferences.
You will need to add implementation 'com.google.code.gson:gson:2.8.6' inside your app gradle dependencies to be able to use Gson.
But, you cannot simply parse the list using Gson to Json or viceversa when you use the Class class. In order to do so, you will need to create your own serializer and deserializer for it. Or you'll face this exception:
java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: com.etc.etc.Level1Activity. Forgot to register a type adapter?
So let's create a custom adapter that implements JsonSerializer and JsonDeserializer. Don't forget to put inside the angle brackets the type we're working with, which is Class.
ClassAdapter class
public class ClassAdapter implements JsonSerializer<Class>, JsonDeserializer<Class> {
#Override
public JsonElement serialize(Class src, Type typeOfSrc, JsonSerializationContext context) {
// Get our class 'src' name
return new JsonPrimitive(src.getName());
}
#Override
public Class deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
// Get class
return Class.forName(json.getAsString());
} catch (ClassNotFoundException e) {
// If class could not be found or did not exists, handle error here...
e.printStackTrace();
}
return null;
}
}
To convert our list to Json String and save it inside SharedPreferences:
// Create new GsonBuilder and register our adapter for Class objects
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Class.class, new ClassAdapter());
// Initialize our list of levels (ie. classes)
List<Class> classes = new ArrayList<>();
classes.add(Level1Activity.class);
classes.add(Level2Activity.class);
classes.add(Level3Activity.class);
classes.add(Level4Activity.class);
classes.add(Level5Activity.class);
// Create Gson from GsonBuilder and convert list to json
Gson gson = gsonBuilder.create();
String json = gson.toJson(classes);
// Save json to SharedPreferences
SharedPreferences sharedPreferences = getSharedPreferences("app_name", MODE_PRIVATE);
sharedPreferences.edit().putString("levels", json).apply();
And to retrieve the list back:
// Retrieve json from SharedPreferences
SharedPreferences sharedPreferences = getSharedPreferences("app_name", MODE_PRIVATE);
String json = sharedPreferences.getString("levels", null);
// Handle here if json doesn't exist yet
if (json == null) {
// ...
}
// Create new GsonBuilder and register our adapter for Class objects
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Class.class, new ClassAdapter());
// Create Gson from GsonBuilder and specify type of list
Gson gson = gsonBuilder.create();
Type type = new TypeToken<ArrayList<Class>>(){}.getType();
// Convert json to list
List<Class> classes = gson.fromJson(json, type);
Hope this helps, happy coding!
I'm retrieving comments from the Reddit API. The model is threaded such that each Comment can internally have a List of Comments, named replies. Here's an example of how a JSON response would look:
[
{
"kind":"Listing",
"data":{
"children":[
{
"data":{
"body":"comment",
"replies":{
"kind":"Listing",
"data":{
"children":[
{
"data":{
"body":"reply to comment",
"replies":""
}
}
]
}
}
}
}
]
}
}
]
Here is how I model this with POJOs. The response above would be considered a List of CommentListings.
public class CommentListing {
#SerializedName("data")
private CommentListingData data;
}
public final class CommentListingData {
#SerializedName("children")
private List<Comment> comments;
}
public class Comment {
#SerializedName("data")
private CommentData data;
}
public class CommentData {
#SerializedName("body")
private String body;
#SerializedName("replies")
private CommentListing replies;
}
Note how the bottom level CommentData POJO refers to another CommentListing called "replies".
This model works until GSON reaches the last child CommentData where there are no replies. Rather than providing a null, the API is providing an empty String. Naturally, this causes a GSON exception where it expects an object but finds a String:
"replies":""
Expected BEGIN_OBJECT but was STRING
I attempted to create a custom deserializer on the CommentData class, but due to the recursive nature of the model it seems not to reach the bottom levels of the model. I imagine this is because I'm using a separate GSON instance to complete deserialization.
#Singleton
#Provides
Gson provideGson() {
Gson gson = new Gson();
return new GsonBuilder()
.registerTypeAdapter(CommentData.class, new JsonDeserializer<CommentData>() {
#Override
public CommentData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject commentDataJsonObj = json.getAsJsonObject();
JsonElement repliesJsonObj = commentDataJsonObj.get("replies");
if (repliesJsonObj != null && repliesJsonObj.isJsonPrimitive()) {
commentDataJsonObj.remove("replies");
}
return gson.fromJson(commentDataJsonObj, CommentData.class);
}
})
.serializeNulls()
.create();
}
How can I force GSON to return a null instead of a String so that it doesn't try to force a String into my POJO? Or if that's not possible, manually reconcile the data issue? Please let me know if you need additional context or information. Thanks.
In general your code looks good, but I would recommend a few things:
Your type adapters should not capture Gson instances from outside. Type adapter factories (TypeAdapterFactory) are designed for this purpose. Also, in JSON serializers and deserializers you can implicitly refer it through JsonSerializationContext and JsonDeserializationContext respectively (this avoids infinite recursion in some cases).
Avoid modification JSON objects in memory as much as possible: serializers and deserializers are just a sort of pipes and should not bring you surprises with modified objects.
You can implement a generic "empty string as a null" type deserializer and annotate each "bad" field that requires this kind of deserialization strategy. You might consider it's tedious, but it gives you total control wherever you need it (I don't know if Reddit API has some more quirks like this).
public final class EmptyStringAsNullTypeAdapter<T>
implements JsonDeserializer<T> {
// Let Gson instantiate it itself
private EmptyStringAsNullTypeAdapter() {
}
#Override
public T deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
if ( jsonElement.isJsonPrimitive() ) {
final JsonPrimitive jsonPrimitive = jsonElement.getAsJsonPrimitive();
if ( jsonPrimitive.isString() && jsonPrimitive.getAsString().isEmpty() ) {
return null;
}
}
return context.deserialize(jsonElement, type);
}
}
And then just annotate the replies field:
#SerializedName("replies")
#JsonAdapter(EmptyStringAsNullTypeAdapter.class)
private CommentListing replies;
I am trying to use Gson to deserialize a json array, but am currently getting a JsonSyntaxException. The json string was created by a .NET MVC3 web service using JsonResult (meaning, I am not manually creating the json, it is being created by a library which I know to work on several other platforms).
This is the json:
[{"PostID":1,"StudentID":39,"StudentName":"Joe Blow",
"Text":"Test message.","CreateDate":"\/Date(1350178408267)\/",
"ModDate":"\/Date(1350178408267)\/","CommentCount":0}]
This is the code:
public class Post {
public int PostID;
public int StudentID;
public String StudentName;
public String Text;
public Date CreateDate;
public Date ModDate;
public Post() { }
}
Type listOfPosts = new TypeToken<ArrayList<Post>>(){}.getType();
ArrayList<Post> posts = new Gson().fromJson(json, listOfPosts);
The exception says that the date format is invalid:
com.google.gson.JsonSyntaxException: /Date(1350178408267)/
Anyone know what is going on?
I found an answer here but I found it strange that there isn't an easier way. Several other json libraries I've used support the .NET json format natively. I was surprised when Gson didn't handle it. There must be a better way. If anyone knows of one, please post it here. All the same, this was my solution:
I created a custom JsonDeserializer and registered it for the Date type. By doing so, Gson will use my deserializer for the Date type instead of its default. The same can be done for any other type if you want to serialize/deserialize it in a custom way.
public class JsonDateDeserializer implements JsonDeserializer<Date> {
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String s = json.getAsJsonPrimitive().getAsString();
long l = Long.parseLong(s.substring(6, s.length() - 2));
Date d = new Date(l);
return d;
}
}
Then, when I am creating my Gson object:
Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new JsonDateDeserializer()).create();
Now my gson object will be capable of parsing the .NET date format (millis since 1970).
Another solution is to use ISO 8601 format. This has to be configured on both Gson side as:
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create();
as well as on the server side, e.g. for ASP.NET MVC in Global.asax.cs file, as follows:
JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
serializerSettings.Converters.Add(new IsoDateTimeConverter());
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = serializerSettings;
The advantage of the code above is that it handles both serialization and deserialization and thus allows two way transmission of dates/times.
Note: IsoDateTimeConverter class is part of the JSON.NET library.
Serialize and Deserialize methoda. Register this as a Adapter for GSON
JsonSerializer<Date> ser = new JsonSerializer<Date>() {
#Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext
context) {
return src == null ? null : new JsonPrimitive(src.getTime());
}
};
JsonDeserializer<Date> deser = new JsonDeserializer<Date>() {
#Override
public Date deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
return json == null ? null : new Date(json.getAsLong());
}
};
Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, ser)
.registerTypeAdapter(Date.class, deser).create();
This solution works for me by using SqlDateTypeAdapter:
SqlDateTypeAdapter sqlAdapter = new SqlDateTypeAdapter();
Gson gson = new GsonBuilder()
.registerTypeAdapter(java.sql.Date.class, sqlAdapter)
.setDateFormat("yyyy-MM-dd")
.create();
Ref: https://stackoverflow.com/a/30398307/7308789
I am using google gson-2.2.1 library for parsing large response of JSON.
I have to parse a JSON response where structure may vary.
First case, when the response contains more than one team:
"Details":{
"Role":"abc",
"Team":[
{
"active":"yes",
"primary":"yes",
"content":"abc"
},
{
"active":"yes",
"primary":"yes",
"content":"xyz"
}
],
Second case, when only one team is passed:
"Details":{
"Role":"abc",
"Team":
{
"active":"yes",
"primary":"yes",
"content":"abc"
}
}
There are my base classes used for parsing:
class Details {
public String Role;
public ArrayList<PlayerTeams> Team = new ArrayList<PlayerTeams>();
PlayerTeams Team; // when JsonObject
}
class PlayerTeams {
public String active;
public String primary;
public String content;
}
The problem is that I can not use ArrayList<PlayerTeams> when I have only one of them and it's returned as JsonObject.
Gson can identify static format of JSON response. I can trace full response dynamically by checking if "Team" key is instance of JsonArray or JsonObject but it would be great if a better solution is available for that.
Edit :
If my response is more dynamic..
"Details":{
"Role":"abc",
"Team":
{
"active":"yes",
"primary":"yes",
"content":"abc"
"Test":
{
"key1":"value1",
"key2":"value2",
"key3":"value3"
}
}
}
In my edited question, I am facing problem while my response is more dynamic..Team and Test can be JsonArray or JsonObject.. It really harassing me because sometime Test object may array when more data, may object when single data, string when no data. There is no consistency in response.
You need a type adapter. This adapter would be able to distinguish which format is coming and instance the right object with the right values.
You can do this by:
implement your own type adapter by creating a class that implements JsonSerializer<List<Team>>, JsonDeserializer<List<Team>>, of course JsonSerializer is just needed in case you need to serialize it in that matter too.
Register the type adapter to you GsonBuilder like: new GsonBuilder().registerTypeAdapter(new TypeToken<List<Team>>() {}.getType(), new CoupleAdapter()).create()
The deserialize method could look like:
public List<Team> deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws com.google.gson.JsonParseException {
if (json.isJsonObject()) {
return Collections.singleton(context.deserialize(json, Team.class));
} else {
return context.deserialize(json, typeOfT);
}
}