Gson @AutoValue и необязательный не работают вместе, есть ли обходной путь?

#java #gson #optional #auto-value

#java #gson #необязательно #автоматическое значение

Вопрос:

Gson не имеет прямой поддержки для сериализации классов @AutoValue или для необязательных полей<>, но com.ryanharter.auto.value добавляет @AutoValue, а net.dongliu:gson-java8-datatype добавляет необязательные<> и другие типы java8.

Однако они не работают вместе AFAICT.

Тестовый код:

 public class TestOptionalWithAutoValue {
  private static final Gson gson = new GsonBuilder().serializeNulls()
          // doesnt matter which order these are registered in
          .registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory())
          .registerTypeAdapterFactory(AutoValueGsonTypeAdapterFactory.create())
          .create();

  @Test
  public void testAutoValueOptionalEmpty() {
    AvoTestClass subject = AvoTestClass.create(Optional.empty());

    String json = gson.toJson(subject, AvoTestClass.class);
    System.out.printf("Json produced = %s%n", json);
    AvoTestClass back = gson.fromJson(json, new TypeToken<AvoTestClass>() {}.getType());
    assertThat(back).isEqualTo(subject);
  }

  @Test
  public void testAutoValueOptionalFull() {
    AvoTestClass subject = AvoTestClass.create(Optional.of("ok"));

    String json = gson.toJson(subject, AvoTestClass.class);
    System.out.printf("Json produced = '%s'%n", json);
    AvoTestClass back = gson.fromJson(json, new TypeToken<AvoTestClass>() {}.getType());
    assertThat(back).isEqualTo(subject);
  }
}

@AutoValue
public abstract class AvoTestClass {
  abstract Optional<String> sval();

  public static AvoTestClass create(Optional<String> sval) {
    return new AutoValue_AvoTestClass(sval);
  }

  public static TypeAdapter<AvoTestClass> typeAdapter(Gson gson) {
    return new AutoValue_AvoTestClass.GsonTypeAdapter(gson);
  }
}

@GsonTypeAdapterFactory
public abstract class AutoValueGsonTypeAdapterFactory implements TypeAdapterFactory {
  public static TypeAdapterFactory create() {
    return new AutoValueGson_AutoValueGsonTypeAdapterFactory();
  }
}
 

зависимости gradle:

     annotationProcessor "com.google.auto.value:auto-value:1.7.4"
    annotationProcessor("com.ryanharter.auto.value:auto-value-gson-extension:1.3.1")
    implementation("com.ryanharter.auto.value:auto-value-gson-runtime:1.3.1")
    annotationProcessor("com.ryanharter.auto.value:auto-value-gson-factory:1.3.1")

    implementation 'net.dongliu:gson-java8-datatype:1.1.0'
 

Терпит неудачу с:

 Json produced = {"sval":null}
...
java.lang.NullPointerException: Null sval
...
 

net.dongliu.gson.OptionalAdapter вызывается при сериализации, но не при десериализации.

Мне интересно, есть ли обходной путь, или ответ заключается в том, что Gson должен иметь прямую поддержку для необязательного <> ?

Ответ №1:

Рад видеть, что вы обновили свой вопрос, добавив гораздо больше информации и даже добавив тест! 🙂 Это действительно проясняет ситуацию!

Я не уверен, но в сгенерированном адаптере типов не упоминается значение по умолчанию для sval :

 jsonReader.beginObject();
// [NOTE] This is where it is initialized with null, so I guess it will definitely fail if the `sval` property is not even present in the deserialized JSON object
Optional<String> sval = null;
while (jsonReader.hasNext()) {
String _name = jsonReader.nextName();
// [NOTE] This is where it skips `null` value so it even does not reach to the `OptionalAdapter` run
if (jsonReader.peek() == JsonToken.NULL) {
  jsonReader.nextNull();
  continue;
}
switch (_name) {
  default: {
    if ("sval".equals(_name)) {
      TypeAdapter<Optional<String>> optional__string_adapter = this.optional__string_adapter;
      if (optional__string_adapter == null) {
        this.optional__string_adapter = optional__string_adapter = (TypeAdapter<Optional<String>>) gson.getAdapter(TypeToken.getParameterized(Optional.class, String.class));
      }
      sval = optional__string_adapter.read(jsonReader);
      continue;
    }
    jsonReader.skipValue();
  }
}
}
jsonReader.endObject();
return new AutoValue_AvoTestClass(sval);
 

Я понятия не имею, есть ли способ настроить значения по умолчанию в AutoValue или других генераторах, которые вы упомянули, но это похоже на ошибку.

Если нет никакого способа обойти это (скажем, разработка библиотеки прекращена; требуется слишком много времени, чтобы ждать исправления; что угодно), вы всегда можете реализовать это самостоятельно, однако с некоторыми затратами времени выполнения (в основном, так Gson работает под капотом для объектов пакета данных). Идея заключается в делегировании задания встроенному RuntimeTypeAdapterFactory , чтобы он мог работать с конкретным классом, а не с абстрактным, и устанавливать все поля в соответствии с зарегистрированными адаптерами типов (чтобы также поддерживались типы Java 8). Стоимость здесь — отражение, поэтому этот адаптер может работать медленнее, чем сгенерированные адаптеры типов. Другое дело, что если свойство JSON даже не встречается в объекте JSON, соответствующее поле останется null . Для этого требуется другой адаптер типа после десериализации.

 final class SubstitutionTypeAdapterFactory
        implements TypeAdapterFactory {

    private final Function<? super Type, ? extends Type> substitute;

    private SubstitutionTypeAdapterFactory(final Function<? super Type, ? extends Type> substitute) {
        this.substitute = substitute;
    }

    static TypeAdapterFactory create(final Function<? super Type, ? extends Type> substitute) {
        return new SubstitutionTypeAdapterFactory(substitute);
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        @Nullable
        final Type substitution = substitute.apply(typeToken.getType());
        if ( substitution == null ) {
            return null;
        }
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> delegateTypeAdapter = (TypeAdapter<T>) gson.getDelegateAdapter(this, TypeToken.get(substitution));
        return delegateTypeAdapter;
    }

}
 
 final class DefaultsTypeAdapterFactory
        implements TypeAdapterFactory {

    private final Function<? super Type, ? extends Type> substitute;
    private final LoadingCache<Class<?>, Collection<Map.Entry<Field, ?>>> fieldsCache;

    private DefaultsTypeAdapterFactory(final Function<? super Type, ? extends Type> substitute, final Function<? super Type, ?> toDefault) {
        this.substitute = substitute;
        fieldsCache = CacheBuilder.newBuilder()
                // TODO tweak the cache
                .build(new CacheLoader<Class<?>, Collection<Map.Entry<Field, ?>>>() {
                    @Override
                    public Collection<Map.Entry<Field, ?>> load(final Class<?> clazz) {
                        // TODO walk hieararchy
                        return Stream.of(clazz.getDeclaredFields())
                                .map(field -> {
                                    @Nullable
                                    final Object defaultValue = toDefault.apply(field.getGenericType());
                                    if ( defaultValue == null ) {
                                        return null;
                                    }
                                    field.setAccessible(true);
                                    return new AbstractMap.SimpleImmutableEntry<>(field, defaultValue);
                                })
                                .filter(Objects::nonNull)
                                .collect(Collectors.toList());
                    }
                });
    }

    static TypeAdapterFactory create(final Function<? super Type, ? extends Type> substitute, final Function<? super Type, ?> toDefault) {
        return new DefaultsTypeAdapterFactory(substitute, toDefault);
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        @Nullable
        final Type substitution = substitute.apply(typeToken.getType());
        if ( substitution == null ) {
            return null;
        }
        if ( !(substitution instanceof Class) ) {
            return null;
        }
        final Collection<Map.Entry<Field, ?>> fieldsToPatch = fieldsCache.getUnchecked((Class<?>) substitution);
        if ( fieldsToPatch.isEmpty() ) {
            return null;
        }
        final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
        return new TypeAdapter<T>() {
            @Override
            public void write(final JsonWriter out, final T value)
                    throws IOException {
                delegateTypeAdapter.write(out, value);
            }

            @Override
            public T read(final JsonReader in)
                    throws IOException {
                final T value = delegateTypeAdapter.read(in);
                for ( final Map.Entry<Field, ?> e : fieldsToPatch ) {
                    final Field field = e.getKey();
                    final Object defaultValue = e.getValue();
                    try {
                        if ( field.get(value) == null ) {
                            field.set(value, defaultValue);
                        }
                    } catch ( final IllegalAccessException ex ) {
                        throw new RuntimeException(ex);
                    }
                }
                return value;
            }
        };
    }

}
 
 @AutoValue
abstract class AvoTestClass {

    abstract Optional<String> sval();

    static AvoTestClass create(final Optional<String> sval) {
        return new AutoValue_AvoTestClass(sval);
    }

    static Class<? extends AvoTestClass> type() {
        return AutoValue_AvoTestClass.class;
    }

}
 
 public final class OptionalWithAutoValueTest {

    private static final Map<Type, Type> autoValueClasses = ImmutableMap.<Type, Type>builder()
            .put(AvoTestClass.class, AvoTestClass.type())
            .build();

    private static final Map<Class<?>, ?> defaultValues = ImmutableMap.<Class<?>, Object>builder()
            .put(Optional.class, Optional.empty())
            .build();

    private static final Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory())
            .registerTypeAdapterFactory(SubstitutionTypeAdapterFactory.create(autoValueClasses::get))
            .registerTypeAdapterFactory(DefaultsTypeAdapterFactory.create(autoValueClasses::get, type -> {
                if ( type instanceof Class ) {
                    return defaultValues.get(type);
                }
                if ( type instanceof ParameterizedType ) {
                    return defaultValues.get(((ParameterizedType) type).getRawType());
                }
                return null;
            }))
            .create();

    @SuppressWarnings("unused")
    private static Stream<Optional<String>> test() {
        return Stream.of(
                Optional.of("ok"),
                Optional.empty()
        );
    }

    @ParameterizedTest
    @MethodSource
    public void test(final Optional<String> optional) {
        final AvoTestClass before = AvoTestClass.create(optional);
        final String json = gson.toJson(before, AvoTestClass.class);
        final AvoTestClass after = gson.fromJson(json, AvoTestClass.class);
        Assert.assertEquals(before, after);
    }

}
 

Это решение в значительной степени основано на отражении, но это просто обходной путь, если генераторы не могут выполнить эту работу (опять же, не уверен, что их можно настроить так, чтобы таких проблем не было).