问题
I wish to have a custom GSON deserializer such that whenever it is deserializing a JSON object (i.e. anything within curly brackets { ... }
), it will look for a $type
node and deserialize using its inbuilt deserializing capability to that type. If no $type
object is found, it just does what it normal does.
So for example, I would want this to work:
{
"$type": "my.package.CustomMessage"
"payload" : {
"$type": "my.package.PayloadMessage",
"key": "value"
}
}
public class CustomMessage {
public Object payload;
}
public class PayloadMessage implements Payload {
public String key;
}
Calling: Object customMessage = gson.fromJson(jsonString, Object.class)
.
So currently if I change the payload
type to the Payload
interface:
public class CustomMessage {
public Payload payload;
}
Then the following TypeAdapaterFactory
will do what I want:
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
final PojoTypeAdapter thisAdapter = this;
public T read(JsonReader reader) throws IOException {
JsonElement jsonElement = (JsonElement)elementAdapter.read(reader);
if (!jsonElement.isJsonObject()) {
return delegate.fromJsonTree(jsonElement);
}
JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonElement typeElement = jsonObject.get("$type");
if (typeElement == null) {
return delegate.fromJsonTree(jsonElement);
}
try {
return (T) gson.getDelegateAdapter(
thisAdapter,
TypeToken.get(Class.forName(typeElement.getAsString()))).fromJsonTree(jsonElement);
} catch (ClassNotFoundException ex) {
throw new IOException(ex.getMessage());
}
}
However, I would like it to work when payload
is of type Object
or any type for that matter, and throw some sort of type match exception if it can't assign the variable.
回答1:
I don't know how you can achieve it with Gson but you have such a feature in Genson by default.
To enable it just do:
Genson genson = new Genson.Builder().setWithClassMetadata(true).create();
You can also register aliases for your class names:
Genson genson = new Genson.Builder().addAlias("myClass", my.package.SomeClass.class).create();
This has however some limitations:
- at the moment you can't change the key used to identify the type, it is @class
- it must be present in your json before the other properties - but looks fine as it is the case in your examples
- Works only with json objects and not arrays or litterals
回答2:
Looking at the source for Gson, I have found what I think is the issue:
// built-in type adapters that cannot be overridden
factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
factories.add(ObjectTypeAdapter.FACTORY);
// user's type adapters
factories.addAll(typeAdapterFactories);
As you can see the ObjectTypeAdapter
will take precedence over my factory.
The only solution as far as I can see is to use reflection to remove the ObjectTypeAdapter
from the list or insert my factory before it. I have done this and it works.
回答3:
This code skeleton works on your example but should be improved and tested with different scenarios.
public class PojoTypeAdapaterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
// check types we support
if (type.getRawType().isAssignableFrom(CustomMessage.class) || type.getRawType().isAssignableFrom(PayloadMessage.class)) {
return new PojoTypeAdapter<T>(gson, type);
}
else return null;
}
private class PojoTypeAdapter<T> extends TypeAdapter<T> {
private Gson gson;
private TypeToken<T> type;
private PojoTypeAdapter(final Gson gson, final TypeToken<T> type) {
this.gson = gson;
this.type = type;
}
public T read(JsonReader reader) throws IOException {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(PojoTypeAdapaterFactory.this, this.type);
final TypeAdapter<JsonElement> elementAdapter = this.gson.getAdapter(JsonElement.class);
JsonElement jsonElement = elementAdapter.read(reader);
if (!jsonElement.isJsonObject()) {
return (T) this.gson.getAdapter(JsonElement.class).fromJsonTree(jsonElement);
}
JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonElement typeElement = jsonObject.get("$type");
if (typeElement == null) {
return delegate.fromJsonTree(jsonElement);
}
try {
final Class myClass = Class.forName(typeElement.getAsString());
final Object myInstance = myClass.newInstance();
final JsonObject jsonValue = jsonElement.getAsJsonObject().get("value").getAsJsonObject();
for (Map.Entry<String, JsonElement> jsonEntry : jsonValue.entrySet()) {
final Field myField = myClass.getDeclaredField(jsonEntry.getKey());
myField.setAccessible(true);
Object value = null;
if (jsonEntry.getValue().isJsonArray()) {
//value = ...;
}
else if (jsonEntry.getValue().isJsonPrimitive()) {
final TypeAdapter fieldAdapter = this.gson.getAdapter(myField.getType());
value = fieldAdapter.fromJsonTree(jsonEntry.getValue());
}
else if (jsonEntry.getValue().isJsonObject()) {
value = this.fromJsonTree(jsonEntry.getValue());
}
myField.set(myInstance, value);
}
return (T) myInstance;
}
catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InstantiationException | NoSuchFieldException | SecurityException e) {
throw new IOException(e);
}
}
@Override
public void write(final JsonWriter out, final T value) throws IOException {
out.beginObject();
out.name("$type");
out.value(value.getClass().getName());
out.name("value");
final TypeAdapter<T> delegateAdapter = (TypeAdapter<T>) this.gson.getDelegateAdapter(PojoTypeAdapaterFactory.this, TypeToken.<T>get(value.getClass()));
delegateAdapter.write(out, value);
out.endObject();
}
}
}
The generated JSON is not exactly the same though, as it contains an additional value
entry:
{
"$type": "my.package.CustomMessage",
"value": {
"payload": {
"$type": "my.package.PayloadMessage",
"value": {
"key": "hello"
}
}
}
}
来源:https://stackoverflow.com/questions/23695649/gson-custom-serialization