Reputation: 987
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.
Upvotes: 0
Views: 767
Reputation: 1787
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:
ReflectiveTypeAdapterFactory
only);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.
Upvotes: 1