I have an API that returns some arguably useful metadata along with requested data itself. It looks something like this:
{
"success": true,
"messages": [],
/* other metadata */
"result": { /* fields with useful data */ }
}
So, basically I want to serialize only stuff that is nested inside of "result" field, preferably still being able to work with meta (checking "success" on true/false and reading messages might be useful).
I thought I could use JSONObject to separate "result" and other meta, but this pipeline feels like a bit of overhead. Is there a way to do it purely with GSON?
The other problem is that I use Retrofit, which has a very neat workflow with pure GSON. If the above is the only adequate way of dealing with such API, how should I approach integrating it into Retrofit workflow?
to your retrofit builder add:
.addConverterFactory(new GsonConverterFactory(new GsonBuilder()
.registerTypeAdapter(Result.class, new JsonDeserializer<Result>() {
#Override
public Result deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if(!(((JsonObject) json).getAsJsonPrimitive("success")).getAsBoolean()) {
return null;
}
JsonObject result = ((JsonObject) json).getAsJsonObject("result");
return new Gson().fromJson(result, Result.class);
}
}).create()))
there are npe and other checks to do of course :)
Create a POJO with #Expose annotation and use serialization = true/false. If you want to serialize only success, then your POJO would look something like this.
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class POJO {
#SerializedName("success")
#Expose(serialize = true, deserialize = false)
private Boolean success;
///Your getter / setter methods
}
I have used this above with Retrofit and it works fine.
Hope this helps!
EDIT:
Also you need to mention this while creating your Retrofit Service
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.excludeFieldsWithoutExposeAnnotation();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(YOUR_BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gsonBuilder.create()))
.build();
Related
I am writing and android add and using retrofit2 to send data to the server.
I am trying to exclude one field from the the object that is being serialized and sent to the server.
The object is named "Cheer" and I try to exclude the field id from it when being sent to the server. iv'e tried using #Expose(false, false) and explained here and tried to make the field private, but it is still sent to the server. See the api, object and call below. Please note, it workes, the object is added to the server, the only issue is that, id is still sent in the JSON and I need to exclude it from it.
Thanks!!!
public class Cheer {
#Expose(deserialize = false, serialize = false)
private int id;
}
public interface CheersAPI {
String BASE_URL = "url:port";
#POST("/cheers")
Call<Cheer> AddCheer(#Body Cheer cheer);
}
cheersAPI.AddCheer(cheerToAdd).enqueue(new Callback<Cheer>(){
#Override
public void onResponse(Call<Cheer> call, Response<Cheer> response) {
Log.d("in the on response", "done creating a cheer");
}
#Override
public void onFailure(Call<Cheer> call, Throwable t) {
Log.d("failed", "failed to add a cheer here!");
}
});
I assume you're using Gson. You can use transient.
private transient int id;
If you require a more complicated solution, take a look at Gson: How to exclude specific fields from Serialization without annotations
Kotlin
just use the annotation #Transient to exclude from the request any variable in your POJO
Example
data class Group(val group_id: String,
val group_name: String,
#Transient val isChecked:Boolean = false)
Transient documentation
/**
* Marks the JVM backing field of the annotated property as `transient`, meaning that it is not
* part of the default serialized form of the object.
*/
Visit here for details.
Basically,
#Expose will not be regarded by the default Gson instance. In order to utilize it, you'll need to use a custom Gson instance:
GsonBuilder builder = new GsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
Gson gson = builder.create();
but if you do this you'll have to add #Expose to every field in all your model classes or they won't be serialised or deserialised by GSON.
This is how to do it with Retrofit
val gson: Gson = GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()
builder = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
And in your model
// These values are read as JSON objects only in server response
#SerializedName("someField")
#Expose(serialize = false, deserialize = true)
var someField: String? = null
For example here, we will not send JSON object to server (deserialize = false) but we will receive it as response (deserialize = true)
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'm using retrofit2 to handle http request after calling from API. Let me explain this.
I have 2 java class(POJO) created to handle user and lecturer data which is User.java and Lecturer.java respectively. For the response data such as :
{
"users": [
{
"user_id": "28",
"user_email": "john#abc.com",
"user_password": "123"
}
]
}
i can use User.java class to handle this response. Nothing complex in this file, only contains getter and setter method. Same goes to lecturer data, here is the example of lecturer data :
{
"lecturers": [
{
"lecturer_id": "3",
"user_id": "28",
"lecturer_name": "johny2"
}
]
}
i can handle it by using Lecturer.java class.
But the problem is, if the response contains both user and lecturer data on a single json, how to handle it?? . Here is the example of request :
{
"users": [
{
"user_id": "28",
"user_email": "john#abc.com",
"user_password": "123",
"lecturer_id": "3",
"lecturer_name": "johny2"
}
]
}
To solve this problem, i think i need to create another java class that contains both User and Lecturer class on it, unfortunately at here i'm stuck.
This is new file, that i tried to create (Userlecturer.java) :
public class UserLecturer {
User user;
Lecturer lecturer;
// how to implement on this part
}
Here is UserLecturer interface :
public interface UserLecturerInterface {
#GET ( "api/endpoint/here" )
Call<UserLecturer> getLecturerByUserId (#Path( "userId" ) String userId );
}
Appreciated for any helps. Ask me for more inputs if above use case did't clear enough. Thanks
I think the POJO should be:
public class Users {
String userId;
String userEmail;
String userPassword;
String lecturerId;
String lecturerName;
}
Even though there are 2 models inside the JSON, you only need 1 model for Retrofit.
If you really want to split the 1 JSON response into 2 models, I think you have to implement custom JSON converter.
Gson gson = new GsonBuilder()
.registerTypeAdapter(UserLecture.class, new JsonDeserializer<UserLecture>() {
public UserLecture deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray usersJsonArray = json.getAsJsonObject().getAsJsonArray("users");
JsonObject userJsonObject = usersJsonArray.getAsJsonArray().get(0).getAsJsonObject();
User user = new User();
user.setUserId(userJsonObject.get("user_id").getAsString());
user.setUserEmail(userJsonObject.get("user_email").getAsString());
user.setUserPassword(userJsonObject.get("user_password").getAsString());
Lecturer lecturer = new Lecturer();
lecturer.setLecturerId(userJsonObject.get("lecturer_id").getAsString());
lecturer.setLecturerName(userJsonObject.get("lecturer_name").getAsString());
return new UserLecture(lecturer, user);
}
})
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl([YOUR_BASE_URL])
.addConverterFactory(GsonFactoryConverter.create(gson))
.build();
This is some code I use to convert longs to Java Date objects.
Presumably, you can do the same thing for your UserLecture object. You should be able to extract the individual json objects for User and Lecture, create a new UserLecture object and let User and Lecture as objects in it.
Gson gson = new GsonBuilder().registerTypeAdapter(UserLecture.class, new JsonDeserializer<UserLecture>() {
public UserLecture deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject user = json.getAsJsonObject().getAsJsonObject("user");
JsonObject lecture = json.getAsJsonObject().getAsJsonObject("lecture");
return new UserLecture(user, lecture);
}
}).create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonFactoryConverter.create(gson))
.build();
Then inside UserLecture:
public UserLecture(JsonObject userJson, JsonObject lectureJson) {
this.user = new User();
this.user.setUserId(userJson.get("user_id").getAsInt());
this.user.serUserEmail(userJson.get("user_email").getAsString());
//so on.
}
At first let me say that the JSON you need to process here is broken by design so you should urge the guy / department / company to fix it.
Secondly, JSON processors like Jackson allow to parse polymorphic data structures like this easily, but they require some kind of type flag to distinguish one of another type (i.e. type: "user" and type: "lecturer"). There is also a way to do this without such type flags, but there is a lot more hand work involved. The last example here shows how to do it.
Yes, it is one possible solution. Gson ignores all fields, which names doesnt match #SerializedName annotation. So, you may try another solution without creating any more pojo classes- return result as String, and try to parse this string as both classes. If one result is empty- then you have another. But, if both kbjects isnt empty- then original response contain fields from both pojos
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);
}
}
I receive from server a response in this form
{"error":null,"id":1,"result":
{"admin":false,
"firstname":"Jason",
"id":346,"idHotel":109,
"idVendor":null,
"lastname":"Butcher",
"sessionkey":"3c8a17ae47a6d131b1a14b44a1d8f9a9",
"urlAvatar":"avatar_316_mjm.jpg",
"urlThumb":"thumb_316_mjm.jpg"}
}
And want to get the various singles attributes,
for example
Boolean error=..;
String admin=....;
String idHotel=...;
and also the images
You should create a wrapper class like this:
public class Response {
public boolean error;
public int id;
public Result result;
}
public class Result {
...
}
then is simple for you to deserialize the json via gson to you classes:
Response response = new Response();
Gson gson = new Gson();
response = gson.fromJson(response, Response.class);
And there's a lot of other nifty things you can do: https://sites.google.com/site/gson/gson-user-guide
Oh forgot one important thing!
Remember to change the namespace on the Gson library, I had problems get it running on htc telephones. You can do this with jar jar links: http://code.google.com/p/jarjar/downloads/list
docs: http://code.google.com/p/jarjar/wiki/CommandLineDocs