简体   繁体   中英

Same type serialization based on field name in GSON without annotations

Given a class that I cannot modifiy

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

I have a JSON representation of the class that uses two diferent patterns to represent foo and bar.

If the field name is foo, use this pattern, if the field name is bar, use the other pattern.

How can I do this with gson without adding (because I can't) annotations on each field name?

Thanks.

So, as I mentioned in the comments above, Gson type adapters do not have access to the full context of the objects they serialize or deserialize. For example, a type adapter for a single type (hierarchy) does not really know what field it may be applied to (and this is the problem in the post). In order to apply different type adapters for different fields, JsonSerializer and JsonDeserializer can be used (therefore every field must be processed manually that is a tedious job). Another bad thing here is that the ReflectiveTypeAdapterFactory that is supposed to process DTOs like that is not extensible directly but can only be extended via the GsonBuilder interface that is also limited.

However, it is possible to implement a workaround that uses the following algorithm:

  • create an exclusion strategy that always skips special fields on deserialization (this affects the ReflectiveTypeAdapterFactory only);
  • create a type adapter factory that creates type adapters for such special fields;
  • once Gson deserializes the wrapper object, the special fields in the wrapper object are supposed to be skipped but set to null (other other defaults in case of primitives), the post-deserializer type adapter asks injected strategies to deserialize each special field that was previously skipped by the exclusion strategy hence ReflectiveTypeAdapterFactory .

That's the trick.

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, ? super JsonElement, ? extends T> deserialize;

}

The unit test:

@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, (gson, 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"
}

(for simplicity, the bar is simply a reversed string to make it obscure for Java format pattern, but make the test more robust)

Note that this approach is generic (and may fit any other type other than Instant ), requires JSON trees to be buffered in memory when deserializing classes that contain special fields (built-in JsonSerializer and JsonDeserializer do the same so who cares?), and lose some special support for @SerializedName , @JsonAdapter , etc.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM