How to serialize Optional<T> classes with Gson? - java

I have an object with the following attributes.
private final String messageBundle;
private final List<String> messageParams;
private final String actionBundle;
private final Map<String, String> data;
private final Optional<Pair<Integer,TimeUnit>> ttl;
private final Optional<Integer> badgeNumber;
private final Optional<String> collapseKey;
The object is in a library, i would rather not modify it just for serialization purpose, and would like to avoid the cost of creating another DTO.
How can i serialize / unserialize Optional attributes? Optional doesn't have a default constructor (neither apache commons Pair), but i can't use the InstanceCreator, and don't really understand how to create a TypeAdapter that would simply delegate the serialization to the underlying Optional content.

After several hours of gooling and coding - there is my version:
public class OptionalTypeAdapter<E> extends TypeAdapter<Optional<E>> {
public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType = (Class<T>) type.getRawType();
if (rawType != Optional.class) {
return null;
}
final ParameterizedType parameterizedType = (ParameterizedType) type.getType();
final Type actualType = parameterizedType.getActualTypeArguments()[0];
final TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(actualType));
return new OptionalTypeAdapter(adapter);
}
};
private final TypeAdapter<E> adapter;
public OptionalTypeAdapter(TypeAdapter<E> adapter) {
this.adapter = adapter;
}
#Override
public void write(JsonWriter out, Optional<E> value) throws IOException {
if(value.isPresent()){
adapter.write(out, value.get());
} else {
out.nullValue();
}
}
#Override
public Optional<E> read(JsonReader in) throws IOException {
final JsonToken peek = in.peek();
if(peek != JsonToken.NULL){
return Optional.ofNullable(adapter.read(in));
}
in.nextNull();
return Optional.empty();
}
}
You can simple registered it with GsonBuilder like this:
instance.registerTypeAdapterFactory(OptionalTypeAdapter.FACTORY)
Please keep attention that Gson does not set values to your class field if field does not present in json. So you need to set default value Optional.empty() in your entity.

The solution by Ilya ignores type parameters, so it can't really work in the general case. My solution is rather complicated, because of the need to distinguish between null and Optional.absent() -- otherwise you could strip away the encapsulation as a list.
public class GsonOptionalDeserializer<T>
implements JsonSerializer<Optional<T>>, JsonDeserializer<Optional<T>> {
#Override
public Optional<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
final JsonArray asJsonArray = json.getAsJsonArray();
final JsonElement jsonElement = asJsonArray.get(0);
final T value = context.deserialize(jsonElement, ((ParameterizedType) typeOfT).getActualTypeArguments()[0]);
return Optional.fromNullable(value);
}
#Override
public JsonElement serialize(Optional<T> src, Type typeOfSrc, JsonSerializationContext context) {
final JsonElement element = context.serialize(src.orNull());
final JsonArray result = new JsonArray();
result.add(element);
return result;
}
}

Just as an addition to maaartinus solution, the version without the encapsulating list, where Optional.absent is simply serialized as null:
public class GsonOptionalDeserializer<T> implements JsonSerializer<Optional<T>>, JsonDeserializer<Optional<T>> {
#Override
public Optional<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
final T value = context.deserialize(json, ((ParameterizedType) typeOfT).getActualTypeArguments()[0]);
return Optional.fromNullable(value);
}
#Override
public JsonElement serialize(Optional<T> src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.orNull());
}
}

I'll add to Anton Onikiychuk's answer
#Override
public Optional<E> read(JsonReader in) throws IOException {
JsonToken peek = in.peek();
if (peek == JsonToken.NULL) {
in.nextNull(); // consuming JSON null
return Optional.empty();
}
return Optional.ofNullable(adapter.read(in));
}

Related

Parsing single JSON element as array

One of the fields in my JSON response is a String[] when containing more than one element, and a String when it's just one. Like this:
"assets": [
"0901d196804adc1c",
"0901d196804ebd93",
"0901d196804ea5e2"
]
"assets": "0901d196804adc1c"
Ideally, I would like to get a String[] always, so if the JSON type of the element is String, convert it to a String[] with one element.
How can I do that?
If you cannot edit the response on the server side: Please refer to this question and answers, looks quite similar to your situation.
If you can edit the response, just reply always with String arrays (f.e. "assets": ["0901d196804adc1c"]).
You have two options:
1) Implement custom type adapter for gson to handle such situations (preferable solution).
2) Define field of type Object and cast it to the appropriate type at runtime
public static class AssetsContainer{
private Object assets;
public List<String> getAssets() {
if(assets instanceof List<?>) {
return (List<String>) assets;
} else if(assets instanceof String){
return Arrays.asList((String) assets);
} else {
//TODO: handle
return null;
}
}
}
How about using the TypeAdapter API?
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(SingletonListTypeAdapter.FACTORY)
.create();
The following will check for non-array JSON types when expecting array types and try to make them into singleton Java Lists. (Note that this uses Lists, not arrays. You can adapt it if you want, but Effective Java notes that application-layer code should prefer the Collections APIs over arrays.)
final class SingletonListTypeAdapter<T> extends TypeAdapter<List<T>> {
static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
#SuppressWarnings("unchecked")
#Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() != List.class) {
return null;
}
TypeToken<?> collectionElementType = TypeToken.get(
getCollectionElementType((ParameterizedType) type.getType()));
TypeAdapter<List<Object>> delegate = (TypeAdapter<List<Object>>)
gson.getDelegateAdapter(this, collectionElementType);
return (TypeAdapter<T>) new SingletonListTypeAdapter<>(delegate);
}
};
private final TypeAdapter<T> delegate;
SingletonListTypeAdapter(TypeAdapter<T> delegate) {
this.delegate = delegate;
}
#Override public void write(JsonWriter out, List<T> value) throws IOException {
out.beginArray();
for (int i = 0, size = value.size(); i < size; i++) {
delegate.write(out, value.get(i));
}
out.endArray();
}
#Override public List<T> read(JsonReader in) throws IOException {
if (in.peek() != BEGIN_ARRAY) {
return Collections.singletonList(delegate.read(in));
}
in.beginArray();
List<T> expanding = new ArrayList<>();
while (in.hasNext()) {
expanding.add(delegate.read(in));
}
in.endArray();
return Collections.unmodifiableList(expanding);
}
static Type getCollectionElementType(ParameterizedType type) {
Type[] types = type.getActualTypeArguments();
Type paramType = types[0];
if (paramType instanceof WildcardType) {
return ((WildcardType) paramType).getUpperBounds()[0];
}
return paramType;
}
}

How to do conditional Gson deserialization default value

Imagine if I have the following JSON
{"game":"football", "people":"elevent"}
{"game":"badminton", "people":"two"}
My class as below
class Sport {
String game;
String people;
}
I could do a deserialize of my Json as below
Sport mySport = Gson().fromJson(json, Sport.class);
However, if my JSON is only
{"game":"football"}
{"game":"badminton"}
I would like it to automatically initialize people to "elevent" or "two", pending of the first field. Is there a way to configure my GsonBuilder() to have that achieve automatically during deserialization?
You could create a custom JsonDeserializer:
public class SportDeserializer implements JsonDeserializer<Sport> {
#Override
public Sport deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = (JsonObject) json;
String game = jsonObject.get("game").getAsString();
JsonElement nullablePeople = jsonObject.get("people");
String people = nullablePeople == null ? null : nullablePeople.getAsString();
if (people == null || people.isEmpty()) {
if (game.equals("football")) {
people = "elevent";
}
else if (game.equals("badminton")) {
people = "two";
}
}
Sport sport = new Sport();
sport.game = game;
sport.people = people;
return sport;
}
}
And then use the custom JsonDeserializer:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Sport.class, new SportDeserializer());
Gson gson = gsonBuilder.create();
Sport sport = gson.fromJson(jsonString, Sport.class);
My answer below isn't the best for this question since I simplified the question, and the other answer would suite this better.
But for more complicated scenarios, my answer below would help. It is basically setup a post-processing after the GSon converted.
I finally use Gson convert post-processing.
class Sport implements PostProcessingEnabler.PostProcessable {
String game;
String people;
#Override
public void gsonPostProcess() {
// The below is something simple.
// Could have more complicated scneario.
if (game.equals("football")) {
people = "elevant";
} else if (game.equals("badminton")) {
people = "two";
}
}
}
class PostProcessingEnabler implements TypeAdapterFactory {
public interface PostProcessable {
void gsonPostProcess();
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
T obj = delegate.read(in);
if (obj instanceof PostProcessable) {
((PostProcessable) obj).gsonPostProcess();
}
return obj;
}
};
}
}
Tribute goes to https://blog.simplypatrick.com/til/2016/2016-03-02-post-processing-GSON-deserialization/

Gson deserialize JSON array with multiple object types

I have some odd JSON like:
[
{
"type":"0",
"value":"my string"
},
{
"type":"1",
"value":42
},
{
"type":"2",
"value": {
}
}
]
Based on some field, the object in the array is a certain type.
Using Gson, my thought is to have a TypeAdapterFactory that sends delegate adapters for those certain types to a TypeAdapter, but I'm hung up on understanding a good way of reading that "type" field to know which type to create.
In the TypeAdapter,
Object read(JsonReader in) throws IOException {
String type = in.nextString();
switch (type) {
// delegate to creating certain types.
}
}
would assume the "type" field comes first in my JSON. Is there a decent way to remove that assumption?
Here is some code I wrote to handle an array of NewsFeedArticle and NewsFeedAd items in Json. Both items implement a marker interface NewsFeedItem to allow me to easily check if the TypeAdater should be used for a particular field.
public class NewsFeedItemTypeAdapterFactory implements TypeAdapterFactory {
#Override
#SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (!NewsFeedItem.class.isAssignableFrom(type.getRawType())) {
return null;
}
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
TypeAdapter<NewsFeedArticle> newsFeedArticleAdapter = gson.getDelegateAdapter(this, TypeToken.get(NewsFeedArticle.class));
TypeAdapter<NewsFeedAd> newsFeedAdAdapter = gson.getDelegateAdapter(this, TypeToken.get(NewsFeedAd.class));
return (TypeAdapter<T>) new NewsFeedItemTypeAdapter(jsonElementAdapter, newsFeedArticleAdapter, newsFeedAdAdapter).nullSafe();
}
private static class NewsFeedItemTypeAdapter extends TypeAdapter<NewsFeedItem> {
private final TypeAdapter<JsonElement> jsonElementAdapter;
private final TypeAdapter<NewsFeedArticle> newsFeedArticleAdapter;
private final TypeAdapter<NewsFeedAd> newsFeedAdAdapter;
NewsFeedItemTypeAdapter(TypeAdapter<JsonElement> jsonElementAdapter,
TypeAdapter<NewsFeedArticle> newsFeedArticleAdapter,
TypeAdapter<NewsFeedAd> newsFeedAdAdapter) {
this.jsonElementAdapter = jsonElementAdapter;
this.newsFeedArticleAdapter = newsFeedArticleAdapter;
this.newsFeedAdAdapter = newsFeedAdAdapter;
}
#Override
public void write(JsonWriter out, NewsFeedItem value) throws IOException {
if (value.getClass().isAssignableFrom(NewsFeedArticle.class)) {
newsFeedArticleAdapter.write(out, (NewsFeedArticle) value);
} else if (value.getClass().isAssignableFrom(NewsFeedAd.class)) {
newsFeedAdAdapter.write(out, (NewsFeedAd) value);
}
}
#Override
public NewsFeedItem read(JsonReader in) throws IOException {
JsonObject objectJson = jsonElementAdapter.read(in).getAsJsonObject();
if (objectJson.has("Title")) {
return newsFeedArticleAdapter.fromJsonTree(objectJson);
} else if (objectJson.has("CampaignName")) {
return newsFeedAdAdapter.fromJsonTree(objectJson);
}
return null;
}
}
}
You can then register this with Gson using the following code.
return new GsonBuilder()
.registerTypeAdapterFactory(new NewsFeedItemTypeAdapterFactory())
.create();

Using Enums while parsing JSON with GSON

This is related to a previous question that I asked here earlier
JSON parsing using Gson
I am trying to parse the same JSON, but now I have changed my classes a little bit.
{
"lower": 20,
"upper": 40,
"delimiter": " ",
"scope": ["${title}"]
}
My class now looks like:
public class TruncateElement {
private int lower;
private int upper;
private String delimiter;
private List<AttributeScope> scope;
// getters and setters
}
public enum AttributeScope {
TITLE("${title}"),
DESCRIPTION("${description}"),
private String scope;
AttributeScope(String scope) {
this.scope = scope;
}
public String getScope() {
return this.scope;
}
}
This code throws an exception,
com.google.gson.JsonParseException: The JsonDeserializer EnumTypeAdapter failed to deserialized json object "${title}" given the type class com.amazon.seo.attribute.template.parse.data.AttributeScope
at
The exception is understandable, because as per the solution to my previous question, GSON is expecting the Enum objects to be actually be created as
${title}("${title}"),
${description}("${description}");
But since this is syntactically impossible, what are the recommended solutions, workarounds?
I want to expand a bit NAZIK/user2724653 answer (for my case). Here is a Java code:
public class Item {
#SerializedName("status")
private Status currentState = null;
// other fields, getters, setters, constructor and other code...
public enum Status {
#SerializedName("0")
BUY,
#SerializedName("1")
DOWNLOAD,
#SerializedName("2")
DOWNLOADING,
#SerializedName("3")
OPEN
}
}
in the json file you have just a field "status": "N",, where N=0,1,2,3 - depend on the Status values. So that's all, GSON works fine with the values for the nested enum class. In my case i've parsed a list of Items from json array:
List<Item> items = new Gson().<List<Item>>fromJson(json,
new TypeToken<List<Item>>(){}.getType());
From the documentation for Gson:
Gson provides default serialization and deserialization for Enums... If you would prefer to change the default representation, you can do so by registering a type adapter through GsonBuilder.registerTypeAdapter(Type, Object).
Following is one such approach.
import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
public class GsonFoo
{
public static void main(String[] args) throws Exception
{
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(AttributeScope.class, new AttributeScopeDeserializer());
Gson gson = gsonBuilder.create();
TruncateElement element = gson.fromJson(new FileReader("input.json"), TruncateElement.class);
System.out.println(element.lower);
System.out.println(element.upper);
System.out.println(element.delimiter);
System.out.println(element.scope.get(0));
}
}
class AttributeScopeDeserializer implements JsonDeserializer<AttributeScope>
{
#Override
public AttributeScope deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException
{
AttributeScope[] scopes = AttributeScope.values();
for (AttributeScope scope : scopes)
{
if (scope.scope.equals(json.getAsString()))
return scope;
}
return null;
}
}
class TruncateElement
{
int lower;
int upper;
String delimiter;
List<AttributeScope> scope;
}
enum AttributeScope
{
TITLE("${title}"), DESCRIPTION("${description}");
String scope;
AttributeScope(String scope)
{
this.scope = scope;
}
}
Use annotation #SerializedName:
#SerializedName("${title}")
TITLE,
#SerializedName("${description}")
DESCRIPTION
The following snippet removes the need for explicit Gson.registerTypeAdapter(...), using the #JsonAdapter(class) annotation, available since Gson 2.3 (see comment pm_labs).
#JsonAdapter(Level.Serializer.class)
public enum Level {
WTF(0),
ERROR(1),
WARNING(2),
INFO(3),
DEBUG(4),
VERBOSE(5);
int levelCode;
Level(int levelCode) {
this.levelCode = levelCode;
}
static Level getLevelByCode(int levelCode) {
for (Level level : values())
if (level.levelCode == levelCode) return level;
return INFO;
}
static class Serializer implements JsonSerializer<Level>, JsonDeserializer<Level> {
#Override
public JsonElement serialize(Level src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.levelCode);
}
#Override
public Level deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
try {
return getLevelByCode(json.getAsNumber().intValue());
} catch (JsonParseException e) {
return INFO;
}
}
}
}
With GSON version 2.2.2 enum will be marshalled and unmarshalled easily.
import com.google.gson.annotations.SerializedName;
enum AttributeScope
{
#SerializedName("${title}")
TITLE("${title}"),
#SerializedName("${description}")
DESCRIPTION("${description}");
private String scope;
AttributeScope(String scope)
{
this.scope = scope;
}
public String getScope() {
return scope;
}
}
If you really want to use the Enum's ordinal value you can register a type adapter factory to override Gson's default factory.
public class EnumTypeAdapter <T extends Enum<T>> extends TypeAdapter<T> {
private final Map<Integer, T> nameToConstant = new HashMap<>();
private final Map<T, Integer> constantToName = new HashMap<>();
public EnumTypeAdapter(Class<T> classOfT) {
for (T constant : classOfT.getEnumConstants()) {
Integer name = constant.ordinal();
nameToConstant.put(name, constant);
constantToName.put(constant, name);
}
}
#Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return nameToConstant.get(in.nextInt());
}
#Override public void write(JsonWriter out, T value) throws IOException {
out.value(value == null ? null : constantToName.get(value));
}
public static final TypeAdapterFactory ENUM_FACTORY = new TypeAdapterFactory() {
#SuppressWarnings({"rawtypes", "unchecked"})
#Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) {
return null;
}
if (!rawType.isEnum()) {
rawType = rawType.getSuperclass(); // handle anonymous subclasses
}
return (TypeAdapter<T>) new EnumTypeAdapter(rawType);
}
};
}
Then just register the factory.
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(EnumTypeAdapter.ENUM_FACTORY)
.create();
use this method
GsonBuilder.enableComplexMapKeySerialization();

gson invoking standard deserialization in custom deserializer

Is it possible to write a json deserializer in gson that invokes the default behaviour first and then i can do some post processing on my object. For example:
public class FooDeserializer implements JsonDeserializer<Foo> {
public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Foo foo = context.deserialize(json, typeOfT);//Standard deserialization call?????
foo.doSomething();
return foo();
}
}
I am using gson 1.3 (I cannot use any other version as i can only use the versions in the corporate
repository)
thanks
You can do that by implementing custom TypeAdapterFactory for your object (say CustomClass.class) to be deserialized as below.
public class CustomTypeAdapterFactory implements TypeAdapterFactory {
public final TypeAdapter create(Gson gson, TypeToken type) {
return new TypeAdapter() {
#Override
public void write(JsonWriter out, Object value) throws IOException {
JsonElement tree = delegate.toJsonTree(value);
//add code for writing object
}
#Override
public Object read(JsonReader in) throws IOException {
JsonElement tree = elementAdapter.read(in);
//Add code for reading object
}
};
}
}
And then registering it with Gson as
Gson gson = new GsonBuilder().registerTypeAdapter(CustomClass.class,new CustomTypeAdapterFactory()).create();
public class FooDeserializer implements JsonDeserializer<Foo> {
public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Foo foo=new Gson().fromJson(json, Foo.class); // use default Gson object
foo.doSomething();
return foo;
}
Check out http://gsonfire.io
It's a library I made that extends Gson to handle cases like Post-serialization and Post-deserialization
Also it has many other cool features that I've needed over time with Gson.
public class YourDeserializer<Foo> extends FooDeserializer<Foo>
{
public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Foo foo = super.deserialize(json, typeOfT,context);
foo.doSomething(); //put logic
return foo();
}
}
Here's full implementation based on incomplete answer provided by #user1556622 and discussion in code.google.com/p/google-gson/issues/detail?id=43.
As a result we can serialize list of abstract Field objects and smoothly deserialize it independent on concrete implementation of specific Field and its hierarchy depth.
class MyClass { //class which we would like to serialiaze/deserialize
List<Field> fields; //field is an hierarchy of classes
}
/**
* Purpose of this adapter is simple:
* 1) put during serialization in all Field objects additional property describing class
* 2) during deserialization invoke (based on class info) necessary deserializer to create class
*/
public class FieldTypeAdapterFactory implements TypeAdapterFactory {
private static final String CLASS_META_KEY="clz";
Gson gson;
TypeToken<?> type;
TypeAdapter<Field> fieldAdapter;
TypeAdapter<JsonElement> elementAdapter;
TypeAdapterFactory taf;
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (!Field.class.isAssignableFrom(type.getRawType()))
return null; // this class only serializes 'Field' and its subtypes
this.type=type;
this.gson=gson;
this.taf=this;
fieldAdapter = gson.getDelegateAdapter(taf, TypeToken.get(Field.class));
elementAdapter = gson.getAdapter(JsonElement.class);
TypeAdapter<T> result = new FieldTypeAdapter<T>();
result.nullSafe();
return result;
}
class FieldTypeAdapter<T> extends TypeAdapter<T> {
public FieldTypeAdapter() {
}
#Override
public void write(JsonWriter out, Object value) throws IOException {
if(value instanceof Field) {
JsonObject object = fieldAdapter.toJsonTree((Field )value).getAsJsonObject();
object.addProperty(CLASS_META_KEY, value.getClass().getCanonicalName());
elementAdapter.write(out, object);
}
else {
elementAdapter.write(out, (JsonElement) value);
}
}
#Override
public T read(JsonReader in) throws IOException {
JsonObject object = elementAdapter.read(in).getAsJsonObject();
if (object.has(CLASS_META_KEY)) {
String className=object.get(CLASS_META_KEY).getAsString();
try {
Class<?> clz = Class.forName(className);
TypeAdapter<?> adapter = gson.getDelegateAdapter(taf, TypeToken.get(clz));
return (T) adapter.fromJsonTree(object);
}
catch (Exception e) {
return (T )fieldAdapter.fromJsonTree(object);
}
}
else
return (T )elementAdapter.fromJsonTree(object);
}
}
}
Registration of factory:
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new FieldTypeAdapterFactory())
.create();

Categories

Resources