I'm trying to create a custom deserializer in Gson for an object that is really just a wrapper for a bunch of disparate types with a type identifier.
Here's a simplified overview of my problem domain:
I have users sending messages to each other that can contain a variety of unrelated domain objects, and I want to deserialize it to something like:
public class Message {
public String messageType;
public Object messageData;
}
The messageData object is constructed via JavaScript the programmers decided to just jam every object type into one field "messageData". messageData can be any number of domain objects like: User, Video, Website, Picture, which do not share a base class or interface.
So the (simplified) json object could look like:
{ "messageType": "video", "messageData": { "videoId": 1, "videoTitle": "my vid" } }
or
{ "messageType": "picture", "messageData": { "pictureId": 1, "pictureUrl": "http://www.example.com/cat.jpg" } }
The goal would be to take the messageType and use that to choose a proper class to deserialize it into.
I have come up with something like this:
public class MessageJsonDeserializer implements JsonDeserializer<Message> {
#Override
public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
Message message = new Message();
message.messageType = obj.get("messageType").getAsString();
message.messageData = MessageDataMapper.map(message.messageType, obj.get("messageData")); // maps and casts to the correct Video/Picture,Website etc..
return message;
}
}
This seems to work ok, but let's say that Message has a LOT of other fields that could be automatically serialized, then I'd have to manually parse the JsonObject and extract those fields.
Is there a way that I can use a hybrid approach and have Gson automatically serialize the non-Object fields, but use a custom serializer for that messageData field that ALSO takes into account the messageType?
It might be a bit late, but this answer regarding the RuntimeTypeAdapter might help you in solving this:
https://stackoverflow.com/a/15737704/433421
The RuntimeTypeAdapter enables you to evaluate a self-configured type-attribute in the JSON, in your case "messageType", and register its values with provided POJO-classes.
Look at the link provided in this answer to see an usage-example in the javadoc.
GSon would handle serialization and deserialization of those POJO-classes itself, as I understand it.
If those fields in messageData contain arbitrary objects, you would have to register custom JsonDeserializer-instances as you did with MessageJsonDeserializer.
Hope this helps.
Sadly, no. At least not that I was ever able to determine. There is no way to have it do something like "custom deserialize field X, but super.deserialize() for the rest of it". God knows I tried. If it's any consolation, that seems to be true with every Json deserializer I've looked at.
Related
I'm trying to figure out a way to selectively de-serialize specific fields from flickr.
{"photos":{"page":1,"pages":10,"perpage":100,"total":1000,"photo":[{"id":"","owner":"","secret":"","server":"","farm":,"title":"","ispublic":,"isfriend":,"isfamily":0,"url_s":"","height_s":"","width_s":""},...]}
I receive an object that contains two lists (photos and photo) and i would like to model in Java only the id, url_s and title from photo.
I figured out that I can create my java module with #expose annotation for the fields i'm interested in and than use
builder.excludeFieldsWithoutExposeAnnotation();
this way I'll control which fields get deserialized from photo (still not sure it's gonne work that way), but what about photos?
My questions are:
Is there a way to ignore that list? do I have to model a Java class that contains two lists (with their respective fields) just to grab what I need from the second list?
expanding upon the former question, if my module class for photo is:
public class GalleryItem {
#Expose()
private String mCaption;
#Expose()
private String mId;
#Expose()
private String mUrl;
}
can i call gson only on the part i need?
Type galleryListType = new TypeToken<ArrayList<GalleryItems>> (){}.getType();
List<GalleryItems> itemsList = gson.fromJson(jsonString, galleryListType);
can I somehow use setExclusionStrategies to skip the Photos list?
gsonBuilder.setExclusionStrategies(new ExclusionStrategy() {
#Override
public boolean shouldSkipField(FieldAttributes f) {
**return f.getName().contains("photos")**;
}
#Override
public boolean shouldSkipClass(Class<?> incomingClass) {
return ;
}
});
I've already implemented a solution using raw JSONObject/JSONArray but I'm curious regarding using GSON for the task at hand.
Thanks in advance!
Gson provides deserialisation on Java fields. If you mark as a transient your variable, Gson will not serialised to JSON. Gson also provides finer deserialisation control via annotations: #Expose(deserialize = false/true)
The last option would be writing your custom JsonDeserializer<T>
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'm currently having an issue with the deserialization of certain inner-objects, in spring, I initialize all of my objects before outputting them using #ResponseBody.
As an example, this is a response:
[{id:1, location:{id:1, ... extra location data}},
{id:2, location:1}
]
Now, GSON throws an error as it is not able to understand that location:1 refers to the location object already deserialized in the previous object.
Deserialization is done in the following method:
#Override
public void handleReader(Reader reader) {
try {
String json = readerToString(reader);
T object = getGson().fromJson(json, returnType);
handleObject(object);
} catch (Exception e) {
Sentry.captureException(e);
}
}
As an example, this is called through a regular generic class, I'd use the type Event[] as the T generic in order to return an array.
How can I either fix this using Gson or make spring output the full data every time? Ideally I'd like to fix with Gson as it would allow for seriously reduced bandwidth but I'm not too fussed at this point.
My Spring returning method is as follows:
#Override
public List<T> list() {
return service.findAll();
}
with the initialization like so:
#Override
#Transactional
public List<Event> findAll() {
List<Event> list = eventRepository.findByArchivedFalse();
for (Event event : list) {
this.initialize(event);
}
return list;
}
#Override
public Event initialize(Event obj) {
Hibernate.initialize(obj.getLocation());
Hibernate.initialize(obj.getLocation().get... inner data here);
return obj;
}
I imagine this is going to require a real structure review but, if I can help it, I'd like to keep the structure roughly the same.
You're going to have to write a custom deserializer, if you're not willing to change the JSon. However, changing the JSon is exactly what I would recommend.
Option 1: Changing the JSon
I think the right thing to do is to have two separate messages, e.g.
{
"uniqueLocations":
[
{"id":1, ... extra location details} ,
],
"locationMap":
[
{"id":1,"location":1},
{"id":2,"location":1}
... etc.
]
}
This is clearer; this separates your json so that you always have the same types of data in the same places.
Option 2: Making Gson able to do more complicated deserializations
However, if you're not willing to do that, you could write a custom deserializer. The most straightforward way to do that, extending TypeAdapter, only uses specific, concrete classes, not parameterized types. However, if you want to use a parameterized type, you must use a TypeAdapterFactory.
You can read more about how to do this here: How do I implement TypeAdapterFactory in Gson?
Deserializing works fine if I just pass my custom object through
#POST
public Response saveCustomObject(CustomObject data)
{
// Prints correct value
System.out.println(data);
}
However, if it is a property on another object, it just gets the default value of my custom object
#POST
public Response saveCustomObjectWrapper(CustomObjectWrapper data)
{
// Prints incorrect value
System.out.println(data.getCustomObject());
}
My provider is registered and looks like this:
public CustomObject readFrom(Class<CustomObject> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm, InputStream in) throws IOException, WebApplicationException
{
try {
return new CustomObject(IOUtils.toString(in));
} catch (Exception ex) {
throw new ProcessingException("Error deserializing a CustomObject.", ex);
}
}
The problem is that the reader for all other objects doesn't do lookup/delegation while unmarshalling. What I mean by that, can be seen in this answer, where one reader looks up another reader based on the type. Assuming the format is JSON, whether you're using MOXy (the default with Glassfish) or Jackson, the result is the same. The reader is smart enough to handle the the JSON by itself, so doesn't need to lookup any other readers.
One solution would be to create another reader for the wrapper class, and do lookup/delegation, as seen in the link above. If you have a lot of these situations, you may can extend the default reader, and override its unmarshalling method, but I would completely advise against this, unless you really know what you're doing.
Another solution, depending on the serializer you're using, is to write JsonDeserializer (for Jackson) or XmlAdapter (for MOXy or Jackson). For Jackson an example would be something like (you can see a better example here)
public class CustomObjectDeserializer extends JsonDeserializer<CustomObject> {
#Override
public CustomObject deserialize(JsonParser jp, DeserializationContext dc)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return new CustomObject("Hello World");
}
}
#JsonDeserialize(using = CustomObjectDeserializer.class)
public class CustomObject {
public String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public CustomObject(String message) { this.message = message; }
public CustomObject(){}
}
In which case, there is no need for a custom reader at all. This will handle CustomObjects and objects that have CustomObject as a member. One problem with this is I'm not sure how or if you can get the InputStream. You just need to use the Jackson APIs to parse the JSON.
If you want to use Jackson instead of the default MOXy for glassfish, you can just add the Jackson dependency
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.13</version>
</dependency>
Then register the JacksonFeature, or simply disable MOXy, as mentioned here. If you want to continue using MOXy, I don't know if there is such thing as a class level adapter, so you will still need the reader as well as create a XmlAdapter for class members. It's a bit of a hassle, but that's why I recommend Jackson, for many other reasons, besides this particular use case. You can see an example of an adapter here
Now a lot of this answer is based on the assumption you are using JSON format, as you haven't specified the media type you are using. If it some other format, then I think maybe your only solution is to create another customer reader for the wrapper.
I've been fighting with JSON parsing using GSON today and a lot of things went well with a minimum amount of hassle.
Though, for the following JSON string I started running into some issues;
{"success":1,"response":{"dvds":{"IronMan":{"rating":"awesome"},"BatMan":{"rating":"awesome"},"Smurfs":{"rating":"childish"}}}}
In this JSON I have a collection "response" containing a collection of responsetypes. In the example the only responsetype included is dvds.
But my issue lies here; I wish to have every child of "dvds" to be parsed to a List response , each child being a single Dvd-class object (containing a String "title" and a String "rating")
Thus far parsing seperate values/objects and parsing arrays went with no real issues, but I can't wrap my head around how to solve parsing such collections.
googling today kept referencing me to "TypeTokens" but looking at the code in various topics regarding this issue, I still haven't understood how to implement it for my use-scenario at all.
So, to be concrete;
"How can I make GSON correctly recognize both my responsetype and dvd-collections?"
Currently my ResponseData.class looks like this:
public class ResponseData {
public int success;
public List<ResponseTypes> responsetypes;
public class ResponseType{
public List<Dvd> Dvds;
}
public class Dvd{
public String title;
public String rating;
}
}
That's how i'd look at this problem in an array-based form, but this time i need to apply it in the case i run into a collection of objects instead. in all scenarios i can think of the Class of the object should be able to be recognized by the name of it's parent.
Currently the parsing is initiated as follows;
try{
Gson gson = new Gson();
Reader reader = new InputStreamReader(stream);
ResponseData responsedata = gson.fromJson(reader, ResponseData.class);
return responsedata;
}
I really hope someone can help me out! Thanks!
you have here some problems:
the class ResponseData should hold List<ResponseType> and not List<ResponseTypes>
the json that you supplied isnt matching the classes above.
an accurate classes for this json are:
public class ResponseData
{
public int success;
public Map<String, Map<String, Dvd>> response;
public class Dvd
{
public String rating;
}
}
in order to describe a list in json you should use: "[ ]". for example: [1,2,3]. you can read more about json here
if you want the json to describe the class hierarchy you describe above. it should looks like:
{"success":1,"responseTypes":[{"Dvds":[{"title":"IronMan","rating":"awesome"},{"title": "BatMan", "rating":"awesome"},{"title":"Smurfs", "rating":"childish"}]}]}
notice that unless you declare a mapping between the variable names and the json varible names then you should use the same names. for example "Dvds" and "responseTypes"