【问题标题】:GSON flat down map to other fieldsGSON flat down 映射到其他字段
【发布时间】:2022-01-21 10:26:08
【问题描述】:

所以我有一个使用 Retrofit for API 的 Android 应用程序。我有一个看起来像这样的类:

class Foo {
   String bar;
   Map<String, String> map;
}

当 GSON 创建 JSON 时,它看起来像:

{
   "bar":"value",
   "map": {
      "key1":"value1"
   }
}

是否可以将 JSON 序列化更改为:

{
   "bar":"value",
   "key1":"value1"
}

谢谢。

【问题讨论】:

  • 是什么阻止了您向地图添加 bar-keyed 值并直接序列化地图,甚至不需要将其包装在 Foo 包装器中?
  • 这是一个简单的例子,Foo 要复杂得多..
  • 您必须首先提及这一点,以使您的问题尽可能清晰。另外,如果您能为此付出一些努力,那就太好了。我可以说它可能的,但是如果您添加更多详细信息,那么我们都会知道这是否不是另一个 X/Y 问题,或者如果您走错了路,那么“巢”对你很好。我应该扁平化多少属性/对象,等等。
  • 嗯,我知道我应该在里面放一些代码,但目前我被卡住了。让我用文字来解释。所以我有一个具有一些定义属性 int/String 的类。现在我应该为我的对象添加一些自定义(动态)属性,所以我添加了一个 Map。所有这些属性都应该在一个 JSON 对象中发送,所以没有嵌套。

标签: java android json gson retrofit2


【解决方案1】:

以下是如何使用 Gson 来实现扁平化:

@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();
    }

}

我放了一些 cmets 来解释“whats”和“hows”。但是即使没有评论也很容易理解。以及示例单元测试:

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;
        }

    }

}

上面的代码可以通过使用 Java 8 流、一些 Guava 或 Apache Commons 的东西来简化,但只要你在 Android 上,你可能只需要一些纯 Java 6。

【讨论】:

  • 成功了...谢谢!
  • return delegate.read(jsonReader);有诀窍,但它是正确的吗?
  • 为什么?它的目的完全不同。标记您的字段transient 或使用@Expose 使用正确配置的注释对其进行注释。或者干脆删除该字段。我在这里看不到问题。
  • 从服务器接收 JSON 时,以下语句触发异常 throw new UnsupportedOperationException(); int 适配器读取方法。
  • 您删除了包含更多上下文的注释,现在您错过了从扁平类型适配器引发异常的上下文。委派给read 方法可能可以工作,但它的行为不能与 write 方法相同,因为它不清楚如何“取消扁平化”,因为此操作无法处理以明确的方式在write 方法中生成结果,这就是我让它抛出异常的原因。我认为这很明显......
猜你喜欢
  • 1970-01-01
  • 2015-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多