I can't for the life of me figure out how deserialize this:
{
"c8147c8a-09c3-4165-b5c2-ce72e2c97100": {
"pets": {
"BOOST": [
{
"mcmmoBoost": 15.0,
"owner": "c8147c8a-09c3-4165-b5c2-ce72e2c97100",
"entityType": "IRON_GOLEM",
"health": 150.0,
"tier": 1,
"alive": true
}
]
},
"uuid": "c8147c8a-09c3-4165-b5c2-ce72e2c97100"
}
}
into a Map<UUID, PetPlayer> with PetPlayer containing a multimap called "pets" structured as follows; Multimap<PetType, Pet>. PetType is an enum here and Pet is an abstract class that has multiple implementations.
I tried using these two serializers and deserializers.
First:
public final class HashMultimapAdapter implements JsonSerializer>, JsonDeserializer> {
private static final PetAdapter petAdapter = new PetAdapter();
#Override
public JsonElement serialize(Multimap<PetType, Pet> src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.asMap());
}
#Override
public Multimap<PetType, Pet> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Map<PetType, Collection<JsonElement>> asMap = context.deserialize(json, new TypeToken<Map<PetType, Collection<JsonElement>>>(){{}}.getType());
Multimap<PetType, Pet> multimap = ArrayListMultimap.create();
for (Map.Entry<PetType, Collection<JsonElement>> entry : asMap.entrySet()) {
entry.getValue().forEach(jsonElement -> {
multimap.put(entry.getKey(), petAdapter.deserialize(jsonElement, Pet.class, context));
});
}
return multimap;
}
}
Second:
public class PetAdapter implements JsonSerializer<Pet>, JsonDeserializer<Pet> {
#Override
public Pet deserialize(JsonElement jsonElement, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
EntityType entityType = EntityType.valueOf(jsonElement.getAsJsonObject().get("entityType").getAsString());
switch (entityType) {
case IRON_GOLEM:
return context.deserialize(jsonElement, EcoPet.class);
case WOLF:
return context.deserialize(jsonElement, BoostPet.class);
case MAGMA_CUBE:
return context.deserialize(jsonElement, CombatPet.class);
default:
throw new JsonParseException("Invalid PetType");
}
}
#Override
public JsonElement serialize(Pet src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src);
}
}
This resulted in a stackoverflow.
java.lang.StackOverflowError
at com.google.gson.internal.$Gson$Types.resolve($Gson$Types.java:375) ~[PaperSpigot-1.8.8-R0.1.jar:git-PaperSpigot-"4c7641d"]
I greatly appreciate any help :)
I don't really know a way to do this with gson without it being manually or hacky. This is too big to put as a comment, so I'll leave it here as an answer as an idea to help you out.
First, you get a stack overflow because you are calling context.deserialize on the same parameters which triggers gson to call the same deserializer, which will call again context.deserialize and so on, until the stack overflow.
You'll run into the same problem when serializing because you're also just doing context.serialize.
To avoid this, you'll need to avoid that gson recurses into calling the serializer's/deserializer's methods. This is very easy to achieve by creating another gson instance without the adapters:
public class PetAdapter
implements JsonSerializer<Pet>, JsonDeserializer<Pet> {
private final Gson gson = new Gson();
#Override
public Pet deserialize(JsonElement jsonElement, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
EntityType entityType = EntityType.valueOf(jsonElement.getAsJsonObject().get("entityType").getAsString());
switch (entityType) {
case IRON_GOLEM:
return gson.fromJson(jsonElement, EcoPet.class);
case WOLF:
return gson.fromJson(jsonElement, BoostPet.class);
case MAGMA_CUBE:
return gson.fromJson(jsonElement, CombatPet.class);
default:
throw new JsonParseException("Invalid PetType");
}
}
#Override
public JsonElement serialize(Pet src, Type typeOfSrc, JsonSerializationContext context) {
return gson.toJson(src);
}
}
This works, but only if your Pet implementations do not depend on other custom serializers/deserializers. So as you can imagine this is quite hacky.
Another approach is the manual deserialization. This means that you'd have to go through json element and read the properties like you are reading entityType and manually build your objects.
Very similarly, I guess (I didn't check this), you could first use context to deserialize each pet into a Map of objects and let each pet implement a static method that creates an instance of the specific pet from this map. Something like:
public class IronGolem extends Pet {
public static IronGolem from(Map<String, Object> deserializedPet) {
// here check the map for each thing you need
return new IronGolem(/*pass in every attribute*/);
}
}
Hope this helps.
Related
I am calling a method this way:
GsonBuilder gsonBuilder = new GsonBuilder()
.registerTypeAdapter(CoindeskRateResult.class, CurrencyRateDeserializer.class)
CurrencyRateDeserializer is a com.google.gson.JsonDeserializer
import com.google.gson.JsonDeserializer;
public class CurrencyRateDeserializer implements JsonDeserializer<CoindeskRateResult> {
#Override
public CoindeskRateResult deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
CoindeskRateResult result = new CoindeskRateResult();
return result;
}
}
The error occurs in the GsonBuilder class. I know that typeAdapter is an instance of JsonDeserializer<?> but $Gson$Preconditions.checkArgument throws an IllegalArgumentException
public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) {
$Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?>
|| typeAdapter instanceof JsonDeserializer<?>
|| typeAdapter instanceof InstanceCreator<?>
|| typeAdapter instanceof TypeAdapter<?>);
....
StackTrace:
*Caused by: java.lang.IllegalArgumentException
at com.google.gson.internal.$Gson$Preconditions.checkArgument($Gson$Preconditions.java:46)
at com.google.gson.GsonBuilder.registerTypeAdapter(GsonBuilder.java:472)*
.....
My CoindeskRateResult class:
public class CoindeskRateResult {
public Map<String, String> data = new HashMap<>();
}
This is wrong:
.registerTypeAdapter(CoindeskRateResult.class, CurrencyRateDeserializer.class)
This is why all of the instanceof checks fail for the first case. What you're looking for is:
.registerTypeAdapter(CoindeskRateResult.class, new CurrencyRateDeserializer())
To be honest, I think this is a Gson design flaw.
I have created a deserialization, that whenever it see the String "nil", it will return null.
private static Gson createCustomGson() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(String.class, new JsonDeserializer<String>() {
#Override
public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
return (json.getAsString().equals("nil")) ? null : json.getAsString();
}
});
return gsonBuilder.create();
}
It works good except that, I want to add an exception where for the field "Keyword" that store a List, I don't want to eliminate nil to return null, but retain the String. How to add the exception for "Keyword"?
My Keyword class is of the below type
public class KeywordListing implements Serializable {
List<String> keys;
}
Found a solution. It's by adding another TypeAdapter to use back the default deserizliation from Gson instead as below.
private static Gson createCustomGson() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(String.class, new JsonDeserializer<String>() {
#Override
public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
return (json.getAsString().equals("nil")) ? null : json.getAsString();
}
});
gsonBuilder.registerTypeAdapter(KeywordListing.class, new JsonDeserializer< KeywordListing >() {
#Override
public KeywordListing deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
return new Gson().fromJson(json, KeywordListing.class);
}
});
return gsonBuilder.create();
}
Let me know if there's a shorter answer than what I got above.
is there any easy way to to convert this json:
{
...,
"pictures": [
"url1",
"url2"
],
...
}
to
List<Picture> pictures
where Picture is:
class Picture{
String url;
}
It won't work as above, because I have an exception, saying
Expected BEGIN_OBJECT but was STRING
You need to implement a custom deserializer for this.
Should be looking like this (I didn't try to execute, but that should give you the idea where to start, presumably you have a public constructor with String argument in your Picture.class)
private class PictureDeserializer implements JsonDeserializer<Picture> {
public Picture deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return new Picture(json.getAsJsonPrimitive().getAsString());
}
}
It should be registered:
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(Picture.class, new PictureDeserializer());
For example
public class HistoryRecordDeserializer implements JsonDeserializer<HistoryRecord> {
private LocalDateTimeConverter dateTimeConverter = new LocalDateTimeConverter();
#Override
public HistoryRecord deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
User user = new User();
user.setId(UUUID.fromString(json.get("user").get("id").getAsString()));
OtherData data = new OtherData();
data.setData(json.get("otherData").getAsLong());
return UserAndData(user, otherData);
}
As you can see, I instantiate User and OtherData manually, but I think there is a better solution. What is the best way to deserialize user with fromJson(...)? Should I pass Gson instance to HistoryRecordDeserializer? Should I create new one?
My problem was solved by using JsonDeserializationContext.
#Override
public HistoryRecord deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject object = json.getAsJsonObject();
JsonObject extras = object.get("extraData").getAsJsonObject();
HistoryRecord hr = object.context.deserialize(object.get("data"), HistoryRecord.class);
hr.appendExtraData(extras, HistoryRecordExtraData.class);
...
}
As #varren sad:
If you Gson can deserialize this, then context will be also able to do this.
So, you can even apply another custom type adapter(LocalDateTimeConverter):
gson = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeConverter())
.registerTypeHierarchyAdapter(HistoryRecord.class, new HistoryRecordDeserializer())
.create();
and use it inside HistoryRecordDeserializer:
LocalDateTime localDateTime = context.deserialize(object.get("dateTime"), LocalDateTime.class);
What pattern should I use for date format 1418805300000-0100 ? (Timestamp and timezone)
GsonBuilder().setDateFormat("?????????????-Z")
Solution:
create new GSON with adapters
private static Gson createGson(){
return new GsonBuilder().disableHtmlEscaping()
.registerTypeHierarchyAdapter(Date.class, new DateTimeSerializer())
.registerTypeHierarchyAdapter(Date.class, new DateTimeDeserializer())
.create();
}
public static MyClass fromJson(String json) {
return createGson().fromJson(json, MyClass.class);
}
public String toJson() {
return createGson().toJson(this);
}
JSON Serializer
private static class DateTimeSerializer implements JsonSerializer<Date> {
#Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
// hodgie code
return new JsonPrimitive(src.getTime() + new SimpleDateFormat("Z").format(src));
}
}
Deserializer
private static class DateTimeDeserializer implements JsonDeserializer<Date> {
#Override
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// hodgie code
return new Date(Long.valueOf((json).getAsString().substring(0, 13)));
}
}
GsonBuilder#setDateFormat(String) uses the String provided as an argument for creating SimpleDateFormat instances. SimpleDateFormat does not provide any patterns for generating a timestamp. You won't be able to achieve what you want with setDateFormat. A custom TypeAdapter seems appropriate.