GSON中基于字段名的同类型序列化,无注解

问题描述

给定一个我无法修改的类

class ThirdPartyDTO
{
  Instant foo;
  Instant bar;
  // many more fields.
}

我有一个类的 JSON 表示,它使用两种不同的模式来表示 foo 和 bar。

如果字段名称为 foo,则使用此模式,如果字段名称为 bar,则使用其他模式。

如何在不为每个字段名称添加(因为我不能)注释的情况下使用 gson 执行此操作?

谢谢。

解决方法

因此,正如我在上面的评论中提到的,Gson 类型适配器无法访问它们序列化或反序列化的对象的完整上下文。例如,单个类型(层次结构)的类型适配器并不真正知道它可能应用于哪个字段(这就是帖子中的问题)。为了对不同的字段应用不同类型的适配器,可以使用JsonSerializerJsonDeserializer(因此每个字段都必须手动处理,这是一项繁琐的工作)。另一个不好的地方是,应该处理这样的 DTO 的 ReflectiveTypeAdapterFactory 不能直接扩展,而只能通过 GsonBuilder 接口进行扩展,该接口也受到限制。

但是,可以实现使用以下算法的解决方法:

  • 创建一个在反序列化时总是跳过特殊字段的排除策略(这仅影响 ReflectiveTypeAdapterFactory);
  • 创建一个类型适配器工厂,为此类特殊字段创建类型适配器;
  • 一旦 Gson 反序列化包装器对象,包装器对象中的特殊字段应该被跳过但设置为 null(原语情况下的其他其他默认值),反序列化器类型适配器要求注入的策略反序列化之前被排除策略跳过的每个特殊字段ReflectiveTypeAdapterFactory

这就是诀窍。

interface IPostPatchFactory {

    @Nonnull
    TypeAdapterFactory createTypeAdapterFactory();

    @Nonnull
    ExclusionStrategy createExclusionStrategy();

}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
final class PostPatchFactory
        implements IPostPatchFactory {

    private final Predicate<? super FieldDatum> isFieldPostPatched;
    private final Predicate<? super Class<?>> isClassPostPatched;
    private final Iterable<FieldPatch<?>> fieldPatches;

    static IPostPatchFactory create(final Collection<FieldPatch<?>> fieldPatches) {
        final Collection<FieldPatch<?>> fieldPatchesCopy = new ArrayList<>(fieldPatches);
        final Collection<Field> postPatchedFields = fieldPatches.stream()
                .map(FieldPatch::getField)
                .collect(Collectors.toList());
        final Collection<FieldDatum> postPatchedFieldAttributes = postPatchedFields.stream()
                .map(FieldDatum::from)
                .collect(Collectors.toList());
        final Collection<? super Class<?>> isClassPostPatched = postPatchedFieldAttributes.stream()
                .map(fieldDatum -> fieldDatum.declaringClass)
                .collect(Collectors.toList());
        return new PostPatchFactory(postPatchedFieldAttributes::contains,isClassPostPatched::contains,fieldPatchesCopy);
    }

    @Nonnull
    @Override
    public TypeAdapterFactory createTypeAdapterFactory() {
        return new PostPatchTypeAdapterFactory(isClassPostPatched,fieldPatches);
    }

    @Nonnull
    @Override
    public ExclusionStrategy createExclusionStrategy() {
        return new PostPatchExclusionStrategy(isFieldPostPatched);
    }

    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    private static final class PostPatchTypeAdapterFactory
            implements TypeAdapterFactory {

        private final Predicate<? super Class<?>> isClassPostPatched;
        private final Iterable<FieldPatch<?>> fieldPatches;

        @Override
        @Nullable
        public <T> TypeAdapter<T> create(final Gson gson,final TypeToken<T> typeToken) {
            final Class<? super T> rawType = typeToken.getRawType();
            if ( !isClassPostPatched.test(rawType) ) {
                return null;
            }
            return new PostPatchTypeAdapter<>(gson,gson.getDelegateAdapter(this,typeToken),fieldPatches)
                    .nullSafe();
        }

        @AllArgsConstructor(access = AccessLevel.PRIVATE)
        private static final class PostPatchTypeAdapter<T>
                extends TypeAdapter<T> {

            private final Gson gson;
            private final TypeAdapter<T> delegateTypeAdapter;
            private final Iterable<FieldPatch<?>> fieldPatches;

            @Override
            public void write(final JsonWriter out,final T value) {
                throw new UnsupportedOperationException("TODO");
            }

            @Override
            public T read(final JsonReader in) {
                final JsonElement bufferedJsonElement = JsonParser.parseReader(in);
                final T value = delegateTypeAdapter.fromJsonTree(bufferedJsonElement);
                for ( final FieldPatch<?> fieldPatch : fieldPatches ) {
                    final Field field = fieldPatch.getField();
                    final BiFunction<? super Gson,? super JsonElement,?> deserialize = fieldPatch.getDeserialize();
                    final Object fieldValue = deserialize.apply(gson,bufferedJsonElement);
                    try {
                        field.set(value,fieldValue);
                    } catch ( final IllegalAccessException ex ) {
                        throw new RuntimeException(ex);
                    }
                }
                return value;
            }

        }

    }

    private static final class PostPatchExclusionStrategy
            implements ExclusionStrategy {

        private final Predicate<? super FieldDatum> isFieldPostPatched;

        private PostPatchExclusionStrategy(final Predicate<? super FieldDatum> isFieldPostPatched) {
            this.isFieldPostPatched = isFieldPostPatched;
        }

        @Override
        public boolean shouldSkipField(final FieldAttributes fieldAttributes) {
            return isFieldPostPatched.test(FieldDatum.from(fieldAttributes));
        }

        @Override
        public boolean shouldSkipClass(final Class<?> clazz) {
            return false;
        }

    }

    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @EqualsAndHashCode
    private static final class FieldDatum {

        private final Class<?> declaringClass;
        private final String name;

        private static FieldDatum from(final Member member) {
            return new FieldDatum(member.getDeclaringClass(),member.getName());
        }

        private static FieldDatum from(final FieldAttributes fieldAttributes) {
            return new FieldDatum(fieldAttributes.getDeclaringClass(),fieldAttributes.getName());
        }

    }

}
@AllArgsConstructor(staticName = "of")
@Getter
final class FieldPatch<T> {

    private final Field field;
    private final BiFunction<? super Gson,? extends T> deserialize;

}

单元测试:

@AllArgsConstructor(access = AccessLevel.PACKAGE)
@EqualsAndHashCode
@ToString
final class ThirdPartyDTO {

    private final Instant foo;
    private final Instant bar;

}
public final class PostPatchFactoryTest {

    private static final Collection<FieldPatch<?>> fieldPatches;

    static {
        try {
            final Field thirdPartyDtoFooField = ThirdPartyDTO.class.getDeclaredField("foo");
            thirdPartyDtoFooField.setAccessible(true);
            final Field thirdPartyDtoBarField = ThirdPartyDTO.class.getDeclaredField("bar");
            thirdPartyDtoBarField.setAccessible(true);
            fieldPatches = ImmutableList.<FieldPatch<?>>builder()
                    .add(FieldPatch.of(thirdPartyDtoFooField,(gson,jsonElement) -> {
                        final String rawValue = jsonElement.getAsJsonObject()
                                .get("foo")
                                .getAsString();
                        return Instant.parse(rawValue);
                    }))
                    .add(FieldPatch.of(thirdPartyDtoBarField,jsonElement) -> {
                        final String rawValue = new StringBuilder(jsonElement.getAsJsonObject()
                                .get("bar")
                                .getAsString()
                        )
                                .reverse()
                                .toString();
                        return Instant.parse(rawValue);
                    }))
                    .build();
        } catch ( final NoSuchFieldException ex ) {
            throw new AssertionError(ex);
        }
    }

    private static final IPostPatchFactory unit = PostPatchFactory.create(fieldPatches);

    private static final Gson gson = new GsonBuilder()
            .disableInnerClassSerialization()
            .disableHtmlEscaping()
            .addDeserializationExclusionStrategy(unit.createExclusionStrategy())
            .registerTypeAdapterFactory(unit.createTypeAdapterFactory())
            .create();

    @Test
    public void test()
            throws IOException {
        final ThirdPartyDTO expected = new ThirdPartyDTO(Instant.ofEpochSecond(0),Instant.ofEpochSecond(0));
        try ( final JsonReader jsonReader = new JsonReader(new InputStreamReader(PostPatchFactoryTest.class.getResourceAsStream("input.json"))) ) {
            final ThirdPartyDTO actual = gson.fromJson(jsonReader,ThirdPartyDTO.class);
            Assertions.assertEquals(expected,actual);
        }
    }

}
{
    "foo": "1970-01-01T00:00:00Z","bar": "Z00:00:00T10-10-0791"
}

(为简单起见,bar 只是一个反转的字符串,使 Java 格式模式难以理解,但使测试更健壮)

请注意,此方法是通用(并且可能适合除 Instant 之外的任何其他类型),在反序列化包含特殊字段(内置在 JsonSerializerJsonDeserializer 中做同样的事情,谁在乎?),并失去对 @SerializedName@JsonAdapter 等的一些特殊支持。