TL;DR: Gson can't use Pair
because there is no a way to instantiate it (at least Gson is aware of: unless you tell Gson to use Pair.of
).
Sorry to say but you have a few issues with your solution.
- Don't get bound to
ArrayList
at the declaration site where you can use List
.
- Extending
Parameters
from ArrayList
is rather a bad choice (is ArrayList
that special? what if you want LinkedList
in the future? what if you need a Set
instead? which set?) -- prefer aggregation and encapsulate the collection.
Pair
can be replaced with Map.Entry
-- use the most generalized type as possible (similar to the previous item).
- Suprisingly, using a
List
for such pairs/entries may be better than using a Map
(what if there's a need to store multiple keys in multimap fashion?)
- Gson, unfortunately, won't let you serialize/deserialize
Object
easily because there is no way to reconstruct the original object from the JSON (how should Gson know which class to deserialize with whereas there are tens thousand classes? which one is preferrable? why pay for heuristics or baking in type tags right into JSON to let Gson the original object class (including parameterization if possible)?) -- see some more comments on a similar issue at Why GSON fails to convert Object when it's a field of another Object?.
One of possible ways to make it work with Gson is both migrating to string-string pairs (so that Gson could know how to make a JSON roundtrip for both keys and values) and writing a pair-aware type adapter factory.
final class PairTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory instance = new PairTypeAdapterFactory();
private PairTypeAdapterFactory() {
}
static TypeAdapterFactory getInstance() {
return instance;
}
@Override
@Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !Pair.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
final TypeAdapter<Pair<Object, Object>> pairTypeAdapter = resolveTypeAdapter(gson, actualTypeArguments[0], actualTypeArguments[1]);
@SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) pairTypeAdapter;
return typeAdapter;
}
private <L, R> TypeAdapter<Pair<L, R>> resolveTypeAdapter(final Gson gson, final Type leftType, final Type rightType) {
@SuppressWarnings("unchecked")
final TypeAdapter<L> leftTypeAdapter = (TypeAdapter<L>) gson.getDelegateAdapter(this, TypeToken.get(leftType));
@SuppressWarnings("unchecked")
final TypeAdapter<R> rightTypeAdapter = (TypeAdapter<R>) gson.getDelegateAdapter(this, TypeToken.get(rightType));
return new TypeAdapter<Pair<L, R>>() {
@Override
public void write(final JsonWriter out, final Pair<L, R> value)
throws IOException {
out.beginArray();
leftTypeAdapter.write(out, value.getLeft());
rightTypeAdapter.write(out, value.getRight());
out.endArray();
}
@Override
public Pair<L, R> read(final JsonReader in)
throws IOException {
in.beginArray();
final L left = leftTypeAdapter.read(in);
final R right = rightTypeAdapter.read(in);
in.endArray();
return Pair.of(left, right);
}
}
.nullSafe();
}
}
The code above is pretty straight-forward and it also uses arrays to pack pairs into because you might want to store duplicate keys and save some space (additionally, a two-element array is most likely a good carrier for pairs).
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
final class Event {
String type;
List<Pair<String, String>> params;
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
final class JsonFile {
List<Event> events;
}
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(PairTypeAdapterFactory.getInstance())
.create();
@Test
public void test() {
final JsonFile before = new JsonFile();
final List<Pair<String, String>> params = new ArrayList<>();
params.add(Pair.of("xyzzy", "is a magic name"));
params.add(Pair.of("foo", "bar"));
final Event event = new Event("lispy event", params);
before.events = new ArrayList<>();
before.events.add(event);
final String json = gson.toJson(before, JsonFile.class);
final JsonFile after = gson.fromJson(json, JsonFile.class);
Assertions.assertEquals(before, after);
}
The intermediate JSON is as follows:
{
"events": [
{
"type": "lispy event",
"params": [
[
"xyzzy",
"is a magic name"
],
[
"foo",
"bar"
]
]
}
]
}
Another (somewhat crazy) idea on using Gson is implementing an heuristics type adapter for Object
that are annotated with a custom type-use annotation, say, @Heuristics
(these kind of annotations came along with Java 8). This would allow you to parameterize the pairs like this: Pair<String, @Heuristics Object>
, but as far as I know, Gson is unable to carry type use annotations within type tokens, so type adapters factories can't get them and apply a custom type adapter (caution: implementing such a type adapter for Object
, even those that are not annotated, may cause unwanted global effects).
According to your new information on that, I may be implemented like this (without much commentary, but can be reimplemented if there are more another requirements):
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
final class JsonFile {
List<Event<?>> events;
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
final class Event<T> {
String type;
Map<String, T> params;
}
final class EventTypeAdapterFactory
implements TypeAdapterFactory {
private final Map<String, Class<?>> typeMap;
private EventTypeAdapterFactory(final Map<String, Class<?>> typeMap) {
this.typeMap = typeMap;
}
static TypeAdapterFactory getInstance(final Map<String, Class<?>> typeMap) {
return new EventTypeAdapterFactory(typeMap);
}
@Override
@Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !Event.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final Map<String, TypeAdapter<?>> typeAdapterMap = typeMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> gson.getDelegateAdapter(this, TypeToken.getParameterized(Map.class, String.class, e.getValue()))));
final TypeAdapter<Event<?>> typeAdapter = new TypeAdapter<Event<?>>() {
@Override
public void write(final JsonWriter out, final Event<?> value)
throws IOException {
out.beginObject();
out.name("type");
out.value(value.type);
out.name("params");
@Nullable
@SuppressWarnings("unchecked")
final TypeAdapter<Object> paramsTypeAdapter = (TypeAdapter<Object>) typeAdapterMap.get(value.type);
if ( paramsTypeAdapter == null ) {
throw new IllegalStateException(value.type + " is not mapped to a specific type");
}
paramsTypeAdapter.write(out, value.params);
out.endObject();
}
@Override
public Event<?> read(final JsonReader in) {
final JsonObject root = Streams.parse(in)
.getAsJsonObject();
final String type = root.getAsJsonPrimitive("type")
.getAsString();
@Nullable
@SuppressWarnings("unchecked")
final TypeAdapter<Object> paramsTypeAdapter = (TypeAdapter<Object>) typeAdapterMap.get(type);
if ( paramsTypeAdapter == null ) {
throw new IllegalStateException(type + " is not mapped to a specific type");
}
final JsonObject rawParams = root.getAsJsonObject("params");
@SuppressWarnings("unchecked")
final Map<String, ?> params = (Map<String, ?>) paramsTypeAdapter.fromJsonTree(rawParams);
return new Event<>(type, params);
}
}
.nullSafe();
@SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
}
public final class EventTypeAdapterFactoryTest {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(EventTypeAdapterFactory.getInstance(ImmutableMap.of("lispy event", String.class)))
.create();
@Test
public void test() {
final JsonFile before = new JsonFile();
final Map<String, Object> params = new LinkedHashMap<>();
params.put("xyzzy", "is a magic name");
params.put("foo", "bar");
final Event<Object> event = new Event<>("lispy event", params);
before.events = new ArrayList<>();
before.events.add(event);
final String json = gson.toJson(before, JsonFile.class);
System.out.println(json);
final JsonFile after = gson.fromJson(json, JsonFile.class);
Assertions.assertEquals(before, after);
}
}
{
"events": [
{
"type": "lispy event",
"params": {
"xyzzy": "is a magic name",
"foo": "bar"
}
}
]
}