I have a JSON object that looks like this
{
"foo":{
"bar":"bar",
"echo":"echo"
}
}
But then my Java object looks like this:
class Foo {
public String foo2;
}
I would like to serialize echo
directly into foo
. Is something like this possible:
class Foo {
@SerializedName("foo/echo")
public String foo2;
}
Or how can I do this with a custom deserializer?
As an alternative approach you could also create your own type adapter in order to apply JSON expressions to the not existing fields. It could be based on JsonPath if you are free to add new libraries to the project you're working on.
Having such a non-standard type adapter, you could omit an intermediate mapping class binding directly to the missing field:
final class Foo {
// or @JsonPathExpression("foo.echo")
@JsonPathExpression("$.foo.echo")
String foo2;
}
@JsonPathExpression
is a custom annotation and it can be processed yourself (JsonPath
could be a shorter name but it's already occupied by the JsonPath library so not to make confusions):
@Retention(RUNTIME)
@Target(FIELD)
@interface JsonPathExpression {
String value();
}
Type adapters allow to write complicated serialization/deserialization strategies, and one of their features is that they can be combined to write post-processors, so, for example, custom annotations could be processed.
final class JsonPathTypeAdapterFactory
implements TypeAdapterFactory {
// The type adapter factory is stateless so it can be instantiated once
private static final TypeAdapterFactory jsonPathTypeAdapterFactory = new JsonPathTypeAdapterFactory();
private JsonPathTypeAdapterFactory() {
}
static TypeAdapterFactory getJsonPathTypeAdapterFactory() {
return jsonPathTypeAdapterFactory;
}
@Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Pick up the down stream type adapter to avoid infinite recursion
final TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, typeToken);
// Collect @JsonPathExpression-annotated fields
final Collection<FieldInfo> fieldInfos = FieldInfo.of(typeToken.getRawType());
// If no such fields found, then just return the delegated type adapter
// Otherwise wrap the type adapter in order to make some annotation processing
return fieldInfos.isEmpty()
? delegateAdapter
: new JsonPathTypeAdapter<>(gson, delegateAdapter, gson.getAdapter(JsonElement.class), fieldInfos);
}
private static final class JsonPathTypeAdapter<T>
extends TypeAdapter<T> {
private final Gson gson;
private final TypeAdapter<T> delegateAdapter;
private final TypeAdapter<JsonElement> jsonElementTypeAdapter;
private final Collection<FieldInfo> fieldInfos;
private JsonPathTypeAdapter(final Gson gson, final TypeAdapter<T> delegateAdapter, final TypeAdapter<JsonElement> jsonElementTypeAdapter,
final Collection<FieldInfo> fieldInfos) {
this.gson = gson;
this.delegateAdapter = delegateAdapter;
this.jsonElementTypeAdapter = jsonElementTypeAdapter;
this.fieldInfos = fieldInfos;
}
@Override
public void write(final JsonWriter out, final T value)
throws IOException {
// JsonPath can only read by expression, but not write by expression, so we can only write it as it is...
delegateAdapter.write(out, value);
}
@Override
public T read(final JsonReader in)
throws IOException {
// Building the original JSON tree to keep *all* fields
final JsonElement outerJsonElement = jsonElementTypeAdapter.read(in).getAsJsonObject();
// Deserialize the value, not-existing fields will be omitted
final T value = delegateAdapter.fromJsonTree(outerJsonElement);
for ( final FieldInfo fieldInfo : fieldInfos ) {
try {
// Resolving JSON element by a JSON path expression
final JsonElement innerJsonElement = fieldInfo.jsonPath.read(outerJsonElement);
// And convert it to the field type
final Object innerValue = gson.fromJson(innerJsonElement, fieldInfo.field.getType());
// Since now it's what can be assigned to the object field...
fieldInfo.field.set(value, innerValue);
} catch ( final PathNotFoundException ignored ) {
// if no path given, then just ignore the assignment to the field
} catch ( final IllegalAccessException ex ) {
throw new IOException(ex);
}
}
return value;
}
}
private static final class FieldInfo {
private final Field field;
private final JsonPath jsonPath;
private FieldInfo(final Field field, final JsonPath jsonPath) {
this.field = field;
this.jsonPath = jsonPath;
}
// Scan the given class for the JsonPathExpressionAnnotation
private static Collection<FieldInfo> of(final Class<?> clazz) {
Collection<FieldInfo> collection = emptyList();
for ( final Field field : clazz.getDeclaredFields() ) {
final JsonPathExpression jsonPathExpression = field.getAnnotation(JsonPathExpression.class);
if ( jsonPathExpression != null ) {
if ( collection.isEmpty() ) {
collection = new ArrayList<>();
}
field.setAccessible(true);
collection.add(new FieldInfo(field, compile(jsonPathExpression.value())));
}
}
return collection;
}
}
}
Now both Gson and JsonPath must be configured (the latter does not use Gson by default):
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getJsonPathTypeAdapterFactory())
.create();
static {
final JsonProvider jsonProvider = new GsonJsonProvider(gson);
final MappingProvider gsonMappingProvider = new GsonMappingProvider(gson);
Configuration.setDefaults(new Configuration.Defaults() {
@Override
public JsonProvider jsonProvider() {
return jsonProvider;
}
@Override
public MappingProvider mappingProvider() {
return gsonMappingProvider;
}
@Override
public Set<Option> options() {
return EnumSet.noneOf(Option.class);
}
});
}
And how it's used:
final Foo foo = gson.fromJson("{\"foo\":{\"bar\":\"bar\",\"echo\":\"echo\"}}", Foo.class);
System.out.println(foo.foo2);
final String json = gson.toJson(foo);
System.out.println(json);
Output:
echo
{"foo2":"echo"}
Note that this approach has two disadvantages:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With