So I have an Android app which uses Retrofit for API. I have a class like which looks like:
class Foo {
String bar;
Map<String, String> map;
}
When GSON creates a JSON it looks like:
{
"bar":"value",
"map": {
"key1":"value1"
}
}
Would it be possible to change JSON serialization to:
{
"bar":"value",
"key1":"value1"
}
Thanks.
CodePudding user response:
Here is how Gson could be used to implement the flattening:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Flatten {
}
public final class FlatteningTypeAdapterFactory
implements TypeAdapterFactory {
private FlatteningTypeAdapterFactory() {
}
private static final TypeAdapterFactory instance = new FlatteningTypeAdapterFactory();
private static final String[] emptyStringArray = {};
public static TypeAdapterFactory getInstance() {
return instance;
}
@Override
@Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Class<?> rawType = typeToken.getRawType();
// if the class to be serialized or deserialized is known to never contain @Flatten-annotated elements
if ( rawType == Object.class
|| rawType == Void.class
|| rawType.isPrimitive()
|| rawType.isArray()
|| rawType.isInterface()
|| rawType.isAnnotation()
|| rawType.isEnum()
|| rawType.isSynthetic() ) {
// then just skip it
return null;
}
// otherwise traverse the given class up to java.lang.Object and collect all of its fields
// that are annotated with @Flatten having their names transformed using FieldNamingStrategy
// in order to support some Gson built-ins like @SerializedName
final FieldNamingStrategy fieldNamingStrategy = gson.fieldNamingStrategy();
final Excluder excluder = gson.excluder();
final Collection<String> propertiesToFlatten = new HashSet<>();
for ( Class<?> c = rawType; c != Object.class; c = c.getSuperclass() ) {
for ( final Field f : c.getDeclaredFields() ) {
// only support @Flatten-annotated fields that aren't excluded by Gson (static or transient fields, are excluded by default)
if ( f.isAnnotationPresent(Flatten.class) && !excluder.excludeField(f, true) ) {
// and collect their names as they appear from the Gson perspective (see how @SerializedName works)
propertiesToFlatten.add(fieldNamingStrategy.translateName(f));
}
}
}
// if nothing collected, obviously, consider we have nothing to do
if ( propertiesToFlatten.isEmpty() ) {
return null;
}
return new TypeAdapter<T>() {
private final TypeAdapter<T> delegate = gson.getDelegateAdapter(FlatteningTypeAdapterFactory.this, typeToken);
@Override
public void write(final JsonWriter out, final T value)
throws IOException {
// on write, buffer the given value into a JSON tree (it costs but it's easy)
final JsonElement outerElement = delegate.toJsonTree(value);
if ( outerElement.isJsonObject() ) {
final JsonObject outerObject = outerElement.getAsJsonObject();
// and if the intermediate JSON tree is a JSON object, iterate over each its property
for ( final String outerPropertyName : propertiesToFlatten ) {
@Nullable
final JsonElement innerElement = outerObject.get(outerPropertyName);
if ( innerElement == null || !innerElement.isJsonObject() ) {
continue;
}
// do the flattening here
final JsonObject innerObject = innerElement.getAsJsonObject();
switch ( innerObject.size() ) {
case 0:
// do nothing obviously
break;
case 1: {
// a special case, takes some less memory and works a bit faster
final String propertyNameToMove = innerObject.keySet().iterator().next();
outerObject.add(propertyNameToMove, innerObject.remove(propertyNameToMove));
break;
}
default:
// graft each inner property to the outer object
for ( final String propertyNameToMove : innerObject.keySet().toArray(emptyStringArray) ) {
outerObject.add(propertyNameToMove, innerObject.remove(propertyNameToMove));
}
break;
}
// detach the object to be flattened because we grafter the result to upper level already
outerObject.remove(outerPropertyName);
}
}
// write the result
TypeAdapters.JSON_ELEMENT.write(out, outerElement);
}
@Override
public T read(final JsonReader jsonReader) {
throw new UnsupportedOperationException();
}
}
.nullSafe();
}
}
I've put some comments explaining "whats" and "hows". But it would be really easy to understand even without being commented. And example unit test:
public final class FlatteningTypeAdapterFactoryTest {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(FlatteningTypeAdapterFactory.getInstance())
.create();
@Test
public void test() {
final Object source = new Bar(
"foo-value",
Map.of("k1", "v1", "k2", "v2", "k3", "v3"),
"bar-value",
Map.of("k4", "v4")
);
final JsonObject expected = new JsonObject();
expected.add("foo", new JsonPrimitive("foo-value"));
expected.add("k1", new JsonPrimitive("v1"));
expected.add("k2", new JsonPrimitive("v2"));
expected.add("k3", new JsonPrimitive("v3"));
expected.add("bar", new JsonPrimitive("bar-value"));
expected.add("k4", new JsonPrimitive("v4"));
final JsonElement actual = gson.toJsonTree(source);
Assertions.assertEquals(expected, actual);
}
private static class Foo {
private final String foo;
@Flatten
private final Map<String, String> fooMap;
private Foo(final String foo, final Map<String, String> fooMap) {
this.foo = foo;
this.fooMap = fooMap;
}
}
private static class Bar
extends Foo {
private final String bar;
@Flatten
private final Map<String, String> barMap;
private final transient String thisMustNotBeSerialized = "This must not be serialized";
private Bar(final String foo, final Map<String, String> fooMap, final String bar, final Map<String, String> barMap) {
super(foo, fooMap);
this.bar = bar;
this.barMap = barMap;
}
}
}
The code above can be simplified by using Java 8 streams, some Guava or Apache Commons stuff, but as long as you're on Android, you probably need some pure Java 6 only.