Json Serialize and Deserialize complex content using Gson - java

I have a structure that I want to store using JSON in a file. None of the implementation classes will have more significant information than what is given.
public class ItemExample implements IItem{
private ModelMap map;
private String name;
}
public class ModelMap {
private HashMap<Coord, IPartType> map;
}
public class Coord {
private int x,y,z;
}
public class PartExample implements IPartType {
private String name;
private Purity purity;
}
public Enum Purity{
}
I am brand new to creating JSONs, I've been reading up on how Gson works but I am not really understanding how to translate the examples to my case. Most examples assume knowledge of certain aspects that I just don't know yet.
This is what I have started to do for IPartType:
public class PartDeserialize<T> implements JsonDeserializer<T>{
#Override
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject content = json.getAsJsonObject();
String name = content.get("name").getAsString();
Purity purity = Purity.valueOf(content.get("purity").getAsString());
return new Gson().fromJson(content, typeOfT);
}
}
I would appreciate any help.
Possible JSON Example per request:
{
"name" : "sword",
"map" :
{
"map" :
{
"1,1,1" : //string representation of Coord
{
"name" : "blade",
"purity" : "base" // string representation of Purity Enum
},
"0,0,0" :
{
"name" : "handle",
"purity" : "high"
}
}
}
}

Related

Jackson and deserialisation when you don't know the JSON tag name ahead of time?

I want to use Jackson to deserialise my JSON, from Jira, into a set of POJOs. I have most of what I want working beautifully, now I just have to decode the custom field values.
My input JSON looks like:
{
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
"id": "104144",
"self": "https://jira.internal.net/rest/api/2/issue/104144",
"key": "PRJ-524",
"fields": {
"summary": "Redo unit tests to load from existing project",
"components": [],
"customfield_10240": {
"self": "https://jira.internal.net/rest/api/2/customFieldOption/10158",
"value": "Normal",
"id": "10158"
}
}
I can trivially load the summary and components, since I know ahead of time what the name of those JSON elements are, and can define them in my POJO:
#JsonIgnoreProperties({ "expand", "self", "id", })
public class JiraJson
{
private JiraFields fields;
private String key;
public JiraFields getFields()
{
return fields;
}
public String getKey()
{
return key;
}
public void setFields(JiraFields newFields)
{
fields = newFields;
}
public void setKey(String newKey)
{
key = newKey;
}
}
And similarly for JiraFields:
#JsonIgnoreProperties({ "issuetype", "priority", "status" })
public class JiraFields
{
private List<JiraComponent> components;
private String summary;
public List<JiraComponent> getComponents()
{
return components;
}
public String getSummary()
{
return summary;
}
public void setComponents(List<JiraComponent> newComponents)
{
components = newComponents;
}
public void setSummary(String newSummary)
{
summary = newSummary;
}
}
However, the field custom_10240 actually differs depending on which Jira system this is run against, on one it is custom_10240, on another it is custom_10345, so I cannot hard-code this into the POJO. Using another call, it is possible to know at runtime, before the deserialisation starts, what the name of the field is, but this is not possible at compile time.
Assuming that I want to map the value field into a String on JiraFields called Importance, how do I go about doing that? Or perhaps simpler, how to map this Importance onto a JiraCustomField class?
You can use a method annotated with #JsonAnySetter that accepts all properties that are undefined (and not ignored). in case of a Json Object (like the custom field in the question) Jackson passes a Map that contains all the Object properties (it may even contain Map values in case of nested objects). You can now at run time extract whatever properties you want:
#JsonIgnoreProperties({ "issuetype", "priority", "status" })
public class JiraFields
{
private List<JiraComponent> components;
private String summary;
private String importance;
// getter/setter omitted for brevity
#JsonAnySetter
public void setCustomField(String name, Object value) {
System.out.println(name); // will print "customfield_10240"
if (value instanceof Map) { // just to make sure we got a Json Object
Map<String, Object> customfieldMap = (Map<String, Object>)value;
if (customfieldMap.containsKey("value")) { // check if object contains "value" property
setImportance(customfieldMap.get("value").toString());
}
}
}
}
After searching further, I finally found the JsonAlias annotation. This is still defined at compile time, but I had something that I could search further on!
Further searching, and I found PropertyNamingStrategy, which allows you to rename what JSON field name is expected for a setter/field. This has the advantage in that this is done via a method, and the class can be constructed at runtime.
Here is the class that I used to perform this mapping:
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
public final class CustomFieldNamingStrategy
extends PropertyNamingStrategy
{
private static final long serialVersionUID = 8263960285216239177L;
private final Map<String, String> fieldRemapping;
private final Map<String, String> reverseRemapping;
public CustomFieldNamingStrategy(Map<String, String> newFieldRemappings)
{
fieldRemapping = newFieldRemappings;
reverseRemapping = fieldRemapping.entrySet()//
.stream()//
.collect(Collectors.toMap(Map.Entry::getValue,
Map.Entry::getKey));
}
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName)
{
if (field.getDeclaringClass().getName().equals(JiraFields.class.getName()))
{
return reverseRemapping.getOrDefault(defaultName, defaultName);
}
return defaultName;
}
#Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method,
String defaultName)
{
if (method.getDeclaringClass().getName().equals(JiraFields.class.getName()))
{
return reverseRemapping.getOrDefault(defaultName, defaultName);
}
return defaultName;
}
#Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method,
String defaultName)
{
if (method.getDeclaringClass().getName().equals(JiraFields.class.getName()))
{
return reverseRemapping.getOrDefault(defaultName, defaultName);
}
return defaultName;
}
}

Deserialize json array and object to strings with default GSON configuration

I have to deserialize json like this:
{
"key1" : [
{
"hash1" : "value1_1",
"hash2" : "value1_2",
...
"hashN" : "value1_3",
"date" : "dateValue1"
},
{
"hash1" : "value2_1",
"hash2" : "value2_2",
...
"hashN" : "value2_3",
"date" : "dateValue2"
},
...
],
"key2": {
"description" : {
"hash1" : {
"description1" : "some text",
"description2" : "some text",
},
...
"hashN" : {
"description1" : "some text",
"description2" : "some text",
}
}
}
}
That json have set of unknow keys: hash1, hash2, ... hash2, and set of know keys: key1, key2, description, date, description1, description2.
I work with some custom rest client which use default GSON configuration to deserialize jsons to objects. And I can't change that configuration.
Using this rest client looks like this:
restClient.makeRequest(requestData, DataResponse.class, new RestResponseListener<DataResponse>()
{
#Override
public void onSuccessfulResponse(DataResponse responseData)
{
}
});
DataResponse class have to inherit from Response class from rest client package.
That rest client can't deserialize jsons like above so I decide to try deserializing to String or JsonObject and next in onSuccessfulResponse use custom deserializer.
I try to create below class to hold response:
public class DataResponse extends Response
{
private String key1;
private String key2;
public DataResponse()
{
}
}
Unfortunately I get exception:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 1 column 14 path
The question is, how to deserialize array from key1 and object from key2 to strings.
Or maybe is another solution.
Follow Lyubomyr Shaydariv advice I found two solutions.
Using JsonElement class
JsonElement is of course abstract so I have to use subclasses as below
public class DataResponse extends Response
{
private JsonArray key1;
private JsonObject key2;
public DataResponse()
{
}
//getters
}
In rest client callback I handled this data like this:
restClient.makeRequest(requestData, DataResponse.class, new RestResponseListener<DataResponse>()
{
#Override
public void onSuccessfulResponse(DataResponse responseData)
{
Type mapType = new TypeToken<Map<String, String>>(){}.getType();
Gson gson = new Gson();
for(JsonElement element : responseData.getKey1())
{
Map<String, String> map = gson.fromJson(element, mapType);
//do things
}
}
});
Match DataResponse class to json structure
I don't know why it doesn't work previously, but now does:
public class DataResponse extends Response
{
private List<Map<String, String>> key1;
private Key2SchemaClass key2;
public DataResponse()
{
}
public static class Key2SchemaClass
{
private List<Map<String, Description>> description;
}
public static class Description
{
private String description1;
private String description2;
}
//getters
}

Json Deserialization in Java /w Jackson of mixed types, contained in one array

Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}

JSON Deep Mapping

Is it possible to map a field which is deeper in a json-response to a property in an object - in other words: transform a json which hierarchy into a flat object?
For example I would like to annotate the 'user_id' property of the Marker class with 'links.user.id'.
I have looked into GSON and Jackson, but couldn't find a solution.
Json-Response for a Marker:
{
"id": 791,
"name": "Marker42",
"links": {
"user": {
"href": "http://4242.com/users/970",
"id": 970
}
}
Data-Model:
public class Marker {
#SerializedName("id")
private int id;
#SerializedName("name")
private String name;
#SerializedName("links.user.id")
private int user_id;
}
This isn't pretty but you can set your own deserialiser in GSON. I am not as familiar with Jackson but this tutorial shows a very similar method: http://www.baeldung.com/jackson-deserialization
public static class MarkerGSONDeserializer implements JsonDeserializer<Marker>{
#Override
public Marker deserialize(JsonElement data, Type arg1, JsonDeserializationContext arg2) throws JsonParseException {
if(!data.isJsonObject()){
return null;
} else {
JsonObject obj = data.getAsJsonObject();
Marker res = new Marker();
res.setId(obj.get("id").getAsInt());
res.setName(obj.get("name").getAsString());
res.setUserId(((obj.get("links").getAsJsonObject())).get("user").getAsJsonObject()).get("id").getAsInt();
return res;
}
}
}

Deserialize Generic class Jackson or Gson

From the land of .NET I have a generic class define like so..
public class SyncWrapper<T, I>
{
public IList<T> Data { get; set; }
public IList<I> DeleteIds { get; set; }
public DateTime LastSyncDateTime { get; set; }
}
I was able to create an instance of this object from json by simply calling ...
JsonConvert.DeserializeObject<SyncWrapper<T, Guid>>(json);
Now I've been given the task of porting this code over to Java/Android. Having never touched Java before, I've a lot to learn!
Anyway, so far I've tried Gson and Jackson to get the object from json but no joy. I think that I won't be able to call andthing with the <T> involved gson.fromJson(json, SyncWrapper<T, UUID>.class) for example as there is a problem with type Erasure!
My efforts so far have looked like this....
Gson
Gson gson = new Gson();
SyncWrapper<MyClass, UUID> result = gson.fromJson(json, new TypeToken<SyncWrapper<MyClass, UUID>>() { }.getType());
This compiles but the result is an empty SyncWrapper
Jackson
ObjectMapper mapper = new ObjectMapper();
SyncWrapper<MyClass, UUID> result = mapper.readValue(json, new TypeReference<SyncWrapper<MyClass, UUID>>() { });
This compiles but crashes the app when executed!!!
My Java version of SyncWrapper....
public class SyncWrapper<T, I> {
private DateTime lastSyncDateTime;
private Collection<T> data;
private Collection<I> deleteIds;
public Collection<T> getData() {
return data;
}
public void setData(Collection<T> data) {
this.data = data;
}
public Collection<I> getDeleteIds() {
return deleteIds;
}
public void setDeleteIds(Collection<I> deleteIds) {
this.deleteIds = deleteIds;
}
public DateTime getLastSyncDateTime() {
return lastSyncDateTime;
}
public void setLastSyncDateTime(DateTime lastSyncDateTime) {
this.lastSyncDateTime = lastSyncDateTime;
}
}
I've been really thrown in at the deep end by the powers that be (all programming is the same isn't it?), so any help really appreciated.
I'm not precious about which library I use (Gson, Jackson, etc)
Update
An example of the Json that is to be deserialized...
{
"Data": [
{
"Name": "Company A",
"Id": "7d5d236c-c2b5-42dc-aea5-99e6752c8a52"
},
{
"Name": "Company B",
"Id": "44444444-0000-0000-0000-444444444444"
},
{
"Name": "Company C",
"Id": "249a4558-05c6-483f-9835-0056804791c9"
}
],
"DeleteIds": [
"5f7873a6-b2ee-4566-9714-1577b81384f4",
"1f224a39-16c3-441d-99de-8e58fa8f31c2"
],
"LastSyncDateTime": "\/Date(1393580073773+0000)\/"
}
..or this (more often than not, the DeleteIds will be null)...
{
"Data": [
{
"Name": "Company A",
"Id": "7d5d236c-c2b5-42dc-aea5-99e6752c8a52"
},
{
"Name": "Company B",
"Id": "44444444-0000-0000-0000-444444444444"
},
{
"Name": "Company C",
"Id": "249a4558-05c6-483f-9835-0056804791c9"
}
],
"DeleteIds": null,
"LastSyncDateTime": "\/Date(1393580073773+0000)\/"
}
For the above json I would be mapping to a SyncWrapper where T is Company...
public class Company extends ModelBase {
private String name;
public Company(UUID id, String name) {
super(id);
setName(name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Here's the issues:
Your field names in your Java classes don't match the field names in the JSON; capitalization matters. This is why you're getting back absolutely nothing after parsing.
I'm going to go with Gson examples simply because I know that off the top of my head. You can do the same things in Jackson, but I'd need to look them up:
public class SyncWrapper<T, I> {
#SearializedName("LastSyncDateTime")
private DateTime lastSyncDateTime;
#SearializedName("Data")
private Collection<T> data;
#SearializedName("DeleteIds")
private Collection<I> deleteIds;
This tells Gson which fields in Java map to the fields in JSON. You could also go with a field naming policy instead, since it looks like all your fields are upper camel case:
Gson g = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.build();
Now your fields will match up. The next issue is going to be that UUID class. That class in Java is not a string; it's a class that generates UUIDs. Just use String for the type that holds it in your Java class.
The DateTime class ... same issue. And on top of that you've got a bit of a weird value in your JSON for the date. You'll either want to store that as a String as well, or you're going to have to write a custom deserializer to deal with it.
With those changes, I think you're good to go.
Edit to add from the comments: If you really need the Java UUID class rather than just the String representation, you can write a chunk of code that takes care of this for you:
class UUIDDeserializer implements JsonDeserializer<UUID>
{
#Override
public UUID deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException
{
return UUID.fromString(je.getAsString());
}
}
You can then register this with Gson:
Gson g = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.registerTypeAdapter(UUID.class, new UUIDDeserializer())
.build();
This will populate the UUID typed fields in your class with UUID instances. This is the same thing you'd need to do with that funky date value.
I suggest using Jackson for this; it has a more clear API and does not require creating a new type as Gson (where you have to extend a class to be able to do that).
Example:
public static <T> T fromJsonToGenericPojo(
String json, Class<?> classType, Class<?>... genericTypes) {
JavaType javaType = TypeFactory.defaultInstance()
.constructParametricType(classType, genericTypes);
try {
return OBJECT_MAPPER.readValue(json, javaType);
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
throw new IllegalArgumentException(e);
}
}

Categories

Resources