I've been using Retrofit 2 with some POJO objects for a while now. It's a lovely library and works very well, but it's necessitating some horrendous and messy models that I want to get rid of.
I'll show you... I have the following JSON to peruse:
{
"things": [
{
"thing": {
"id": 823,
"created_at": "2016-02-09T22:55:07.153Z",
"published_at": "2016-02-10T19:23:42.666Z",
"downloads": 16073,
"size": 10716291
}
},
],
"count": 4,
"links": {}
}
Using the POJO Schema generator this creates unnecessary classes that make maintaining code hard to do.
This would create:
Things.java
#SerializedName("things")
#Expose
public List<Things_> things = new ArrayList<>();
Things_.java
#SerializedName("thing")
#Expose
private Thing__ thing;
Things__.java
// Insert normal variables and getter/setters here
I've reduced that down a little as it's just for the idea. In my usage I have of course renamed these classes to make them more managable. But I figured there was a way of simply skipping over Thing and Thing_ and allowing me to just return a list of the actual model data (Thing__) and this two of those classes could be removed and "Thing__" could simple be "Thing".
I was right. Gson allows custom deserialization that lets me achieve this end. I threw together a quick Deserializer and used an appropriate TypeToken
Gson gson = new GsonBuilder()
.registerTypeAdapter(new TypeToken<ArrayList<Thing>>(){}.getType(), new AddonDeserializer())
.create();
List<Thing> model = gson.fromJson(jsonString, new TypeToken<ArrayList<Thing>>(){}.getType());
Sure enough, passing this that exact Json above give me a List of Things that were usable.
Enter Retrofit 2! Having added the registerTypeAdapter() to my Retrofit 2 instance (via my gson instance) I now get an error message:
Expected BEGIN_ARRAY but was BEGIN_OBJECT
This is because, probably, my call is:
#Get("end/of/url/here/{slug}.json")
Call<List<Thing>> getThings(#Path("slug") String slug);
My Json starts with an object ("things") which contains an array of "thing" objects. My deserializer had no issues with this:
public class ThingDeserializer implements JsonDeserializer<List<Thing>> {
#Override
public List<Thing> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray array = json.getAsJsonObject().getAsJsonArray("things");
ArrayList<Thing> list = new ArrayList<>();
for (JsonElement anArray : array) {
list.add((Thing) context.deserialize(anArray.getAsJsonObject().get("thing").getAsJsonObject(), Thing.class));
}
return list;
}
}
Anyway, thanks for sticking with this very long question!
What do I need to do differently or how can I manipulate Retrofit to act the same as the Gson Deserializer I wrote? What I have works, but in the interests of learning something new and writing nicer and more maintainable code I want to figure this out. I could just resort to using ResponseBody callbacks and throwing the Json through my Deserializer but there has to be a better method.
Thanks to #JonathanAste I figured it out.
Instead of a Deserializer, I needed a TypeAdapterFactory implementation.
public class ThingTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
JsonElement jsonElement = elementAdapter.read(in);
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject.has("things") && jsonObject.get("things").isJsonArray())
{
jsonElement = jsonObject.get("things");
}
}
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject.has("thing") && jsonObject.get("thing").isJsonObject())
{
jsonElement = jsonObject.get("thing");
}
}
return delegate.fromJsonTree(jsonElement);
}
}.nullSafe();
}
}
And this allows you to then use
#GET("end/of/url/here/{slug}.json")
Call<List<Thing>> getThings(#Path("slug") String slug);
Without issue.
Related
After updating the targetSdk to 31, gson.ToJson started giving empty results for List<File> on android 12 device (vivo v2036). Tried passing TypeToken as well still remains the same. Funny thing is that its working fine on lower androids and on targetSdk 30.
public void save(Context context, List<File> files) {
Gson gson = new Gson();
String json = gson.toJson(files);
//getting json value as "[{}]"
}
Gson has no built-in adapter for java.io.File so it falls back to using reflection. This should be avoided for third-party classes because it accesses their internal fields which makes you dependent on their internal implementation. That internal implementation could change at any point because it is not part of the public API, and can therefore break your JSON serialization logic.
As mentioned by #CommonsWare you can either change the type of the list to List<String> and by doing so only use types for which Gson has built-in adapters. Or you can also solve this by registering a custom TypeAdapter for File:
class FileTypeAdapter extends TypeAdapter<File> {
#Override
public void write(JsonWriter out, File file) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(file.getPath());
}
}
#Override
public File read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
} else {
return new File(in.nextString());
}
}
}
(Coincidentally this custom TypeAdapter also produces more compact JSON because it serializes the File as a JSON string value whereas the reflection-based adapter would serialize it as JSON object with JSON string property.)
You can then register the adapter like this:
Gson gson = new GsonBuilder()
.registerTypeAdapter(File.class, new FileTypeAdapter())
.create();
If that does not solve your issue, you could also try registering the adapter with GsonBuilder.registerTypeHierarchyAdapter. Possibly Android is creating instances of custom File subclasses.
To detect any cases where you accidentally depend on reflection-based serialization for Android classes you can use GsonBuilder.addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_ANDROID) (requires Gson 2.9.1 or newer).
I have this JSON and when trying to access the list I don't know how to pass the value "Serialized", I don't have any value.
I work on android.
Does anyone know how to access?
Thank you.
[
{
"id": “111”,
"dateStart": "2020-02-26T00:00:00+01:00",
"dateEnd": "2020-02-26T01:30:00+01:00",
"sectionId": 0,
"description": “Test”,
"comment": “Test comment”,
"emissionType": “A”,
"priority": 5,
"audios": [],
"idCollection": “1”
},
{
"id": “222”,
"dateStart": "2020-02-26T00:00:00+01:00",
"dateEnd": "2020-02-26T01:30:00+01:00",
"sectionId": 0,
"description": “Test”,
"comment": “Test comment”,
"emissionType": “B”,
"priority": 5,
"audios": [],
"idCollection": “12”
}
]
public static Gson getGSONBuilder()
{
Gson gson = new GsonBuilder().
registerTypeAdapter(Double.class, new JsonSerializer<Double>()
{
#Override
public JsonElement serialize(Double src, Type typeOfSrc,
JsonSerializationContext context) {
if (src == src.longValue())
return new JsonPrimitive("" + src.longValue());
return new JsonPrimitive("" + src);
}
}).create();
return gson;
}
String responseString=getGSONBuilder().toJson(response.body());//response is Retrofit
response
in retrofit just call the ArrayList> in responce you get all the arraylist data into hashmap with its key and value .
for(int i=0;i<responce.body().size;i++){
arrlist.add(responce.body().get(i))
}
I need to modify some fields of object which I receive from server using Retrofit before I store it internally:
For example, server object model
{
"field1":boolean;
"field2":String
}
app object model:
{
"field1":int=boolean?1:0;
"field2":my prefix + String;
}
Should I write my own parser? Or will parsing using GSON would be enough with some interception before object is stored locally?
Why not changing them after deserilization takes place? It's not that clear what you're trying to do but here is a solution.
(1) Write a deserilizer
public class MyDeserializer implements JsonDeserializer<MyModel> {
...
}
(2) Register type adapter
Gson gson = new GsonBuilder()
.registerTypeAdapter(MyModel.class, MyDeserializer)
.create();
(3) Add it to retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(...)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
Alternatively you could just change your field's content after you get your instances.
Using GSON you can write your own JsonDeserializer.
This example shows how translate int in JSON to boolean in your object.
public class BooleanTypeAdapter implements JsonDeserializer<Boolean> {
#Override
public Boolean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
int code = json.getAsInt();
return code != 0;
}
}
And this:
new GsonBuilder().registerTypeAdapter(boolean.class, new BooleanTypeAdapter()).create();
Hope it helps.
I`m trying to convert the next JSON to object:
{
AccountName:"temnoi",
Parts:{
part-0:{
Name:"HOME",
UptimeSeconds:"2143943",
},
part-1:{
Name:"WORK",
UptimeSeconds:"2276958",
}
}
}
The problem is that Parts isn't an array so I don't have any idea how
to obtain them as List or any other data structure.
For now I have such DTO class
public class Info {
private String AccountName;
private List<Parts> Parts;
}
But obviously program crash as there are no array. I use Retrofit2 with GsonConverter.
Can anyone suggest something to solve this problem?
Unfortunately, as I don't have a lot of time, I came with the next solution.
I replace Retrofit2 by OkHTTP and Gson with built-in JSON parser.
After I get a response with OkHttpClient I manually convert JSON to my object.
JSONObject root = new JSONObject(responseFromServer);
JSONObject parts = root.getJSONObject("Parts");
Iterator<String> jsonPartsIterator = parts.keys();
List<Part> partsList = new ArrayList<>();
while (jsonPartsIterator.hasNext()) {
try{
String key = jsonRootIterator.next();
partsList.add(convertPartJsonToObject(computers.getJSONObject(key)));
} catch(Exception e){
// in case if there will be number '0' return empty List
}
}
Here Part convertPartJsonToObject(JSONObject object) is method to convert part-0, part-1... to object which I need.
I'm trying to implement a custom gson serializer/deserialiser for some list of BasicNameValuePair objects.
I saw the partial solution code (for serialization) here:
How do I get Gson to serialize a list of basic name value pairs?
However I wanted to implement also deserialization and I tried my chances and the code is here:
package dto;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.message.BasicNameValuePair;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
public class KeyValuePairSerializer extends TypeAdapter<List<BasicNameValuePair>> {
#Override
public void write(JsonWriter out, List<BasicNameValuePair> data) throws IOException {
out.beginObject();
for(int i=0; i<data.size();i++){
out.name(data.get(i).getName());
out.value(data.get(i).getValue());
}
out.endObject();
}
#Override
public List<BasicNameValuePair> read(JsonReader in) throws IOException {
ArrayList<BasicNameValuePair> list=new ArrayList<BasicNameValuePair>();
in.beginObject();
while (in.hasNext()) {
String key = in.nextName();
String value = in.nextString();
list.add(new BasicNameValuePair(key,value));
}
in.endObject();
return list;
}
}
Code to initialize and fill the list
ArrayList<BasicNameValuePair> postParameters=new ArrayList<BasicNameValuePair>();
postParameters.add(new BasicNameValuePair("some_key","some_value"));
And here is the code to use the new KeyValuePairSerializer class:
GsonBuilder gsonBuilder= new GsonBuilder();
gsonBuilder.registerTypeAdapter(KeyValuePairSerializer.class, new KeyValuePairSerializer());
Gson gson1=gsonBuilder.create();
//serialization works just fine in the next line
String jsonUpdate=gson1.toJson(postParameters, KeyValuePairSerializer.class);
ArrayList<BasicNameValuePair> postParameters2 = new ArrayList<BasicNameValuePair>();
//postParameters2 = gson1.fromJson(jsonUpdate, KeyValuePairSerializer.class); //? how to cast properly
//deserialization throws an error, it can't cast from ArrayList<BasicNameValuePair> to KeyValuePairSerializer
gson1.fromJson(jsonUpdate, KeyValuePairSerializer.class);
The problem is that it throws an exception at the end and I don't know where exactly is the problem and still not sure how to write the last line to get the result back in the new postParameters2 ArrayList.
Adapted from the GSON Collections Examples:
GsonBuilder gsonBuilder= new GsonBuilder();
gsonBuilder.registerTypeAdapter(KeyValuePairSerializer.class, new KeyValuePairSerializer());
Gson gson1=gsonBuilder.create();
Type collectionType = new TypeToken<ArrayList<BasicNameValuePair>>(){}.getType();
ArrayList<BasicNameValuePair> postParameters2 = gson1.fromJson(jsonUpdate, collectionType);
registerTypeAdapter seems to work only for the serializer not for deserializer.
The only way to call the overridden read function of the KeyValuePairSerializer is to call the:
gson1.fromJson(jsonUpdate, KeyValuePairSerializer.class); without saving the result value in a variable. While it will process the function just fine, it will throw an error inside gson class because it will not be able to cast from the ArrayList to the KeyValuePairSerializer. And I kinda understand why (erasure I guess), just don't know how to do it properly.
Anyway I found a workaround to solve this issue.
It seems that instead of registering the gson object and calling registerTypeAdapter and then using gson1.toJson(Object src, Type typeOfSrc) and gson1.fromJson(String json,Class <T> classOfT) I can get away for deserialization with something simpler like:
KeyValuePairSerializer k= new KeyValuePairSerializer();
parametersList = (ArrayList<BasicNameValuePair>)k.fromJson(jsonUpdate);
Both JsonObject's and NameValuePair's behave in a similar way to dictionaries, I don't think you need to convert one into the other if the use case is similar. Additionally JsonObject allows you to treat your values even easier (instead of looping through the array of value pairs to find the key you need to get its value, JsonObject behaves similarly to a Map in a way that you can directly call the name of the key and it'll return the desired property):
jsonObject.get("your key").getAsString(); (getAsBoolean(), getAsInt(), etc).
For your case I'd create a JsonObject from your string, response or stream and then access it as a map (as shown above):
JsonParser parser = new JsonParser();
JsonObject o = (JsonObject)parser.parse("your json string");
I followed this blog for GSON Collection Examples .
Link is simple to understand and implement.
public class TimeSerializer implements JsonSerializer<time> {
/**
* Implementing the interface JsonSerializer.
* Notice that the the interface has a generic
* type associated with it.
* Because of this we do not have ugly casts in our code.
* {#inheritDoc}
*/
public JsonElement serialize(
final Time time,
final Type type,
final JsonSerializationContext jsonSerializationContext) {
/**
* Returning the reference of JsonPremitive
* which is nothing but a JSONString.
* with value in the format "HH:MM"
*/
return new JsonPrimitive(String.format("%1$02d:%2$02d",
time.getHour(), time.getMinute()));
}