如何使用 Retrofit2 和 GSON 转换器解决“古怪”的 JSON API

Posted

技术标签:

【中文标题】如何使用 Retrofit2 和 GSON 转换器解决“古怪”的 JSON API【英文标题】:How to work around a "quirky" JSON API with Retrofit2 and GSON converter 【发布时间】:2021-07-03 12:55:52 【问题描述】:

我正在为第 3 方 JSON REST API 实现客户端。在客户端(我的项目),我使用 Retrofit 和 GSON 作为库。服务器端似乎是用 php 实现的,超出了我的控制范围,即我无法轻松修复服务器。

服务器经常以不同的实际类型响应相同的正式类型。开箱即用,我在反序列化过程中遇到很多 GSON 解析/转换异常,因为 GSON 对正确类型非常挑剔。

在反序列化响应和类型转换方面,如何使 GSON 更加健壮?我已经找到了注释@JsonAdapter,它可能允许我解决服务器的这些怪癖。但由于这似乎是一个典型的 PHP 问题(示例如下),我认为可能已经有 GSON 的库或外观可以解决这些问题。

具体来说,服务器显示以下怪癖:

布尔值:响应中的布尔值报告为实际 JSON 布尔值,即 b : true b : false ,但也报告为 JSON 数字 01 以及文字字符串 "0""1"、@987654328 @,"false"。另外,一个假值也被报告为null(无对象)和文字字符串"null"

数字:响应中的数字报告为实际的 JSON 数字,即 n : 42 ,但也报告为文字字符串"42"。此外,零值也报告为null(无对象)、文字字符串"null"false(布尔值)或文字字符串"false"

对象数组:如果数组是非空的,一切都很好。回复是 arr : [ ... ] GSON 愉快地将其反序列化为 Java List<>。空数组是问题所在。服务器将空数组正确报告为[],但也报告为falsenull

上面的列表只是我迄今为止遇到的怪事的开始。可能还有更多。这就是我犹豫是否要为 GSON 编写可能自己的适配器的方式,因为我担心这个列表很快就会变得无穷无尽。有哪些库已经为那些典型的 PHP 问题实现了转换?

幸运的是,GSON 转换器只需要额外的反序列化鲁棒性,即如果来自服务器的响应被解析为 Java 对象。当 Java 对象被序列化为 JSON 并在请求中发送到服务器时,服务器很乐意接受任何类型,只要 PHP 能够将其转换为所需的类型。


更新

我开始为普通的 Java 类型(intlongboolean)及其 OO 对应部分 IntegerLong 等编写自定义类型适配器 QuirkyBooleanQuirkyInteger . 到目前为止,这很容易。 棘手的部分是实现一个通用列表适配器,它涵盖了第 3 项(见上文)中提到的怪癖,并且可以正确地反序列化列表中的任何类型。 我的问题是 Java 的类型擦除。 也许有人对以下问题有解决方案?

假设以下带注释的 POJO 用于 GSON 序列化/反序列化。

public class Container 
  @SerializeName("foos")
  @JsonAdapter( QuirkyListAdapter<Foo>.class )  // Note: No legal Java syntax
  public List<Foo> foos;
  
  @SerializeName("bars")
  @JsonAdapter( QuirkyListAdapter<Bar>.class )  // Note: No legal Java syntax
  public List<Bar> bars;


public class Foo 
  @SerializeName("str")
  public String str;


public class Bar 
  @SerializeName("n")
  public Integer n;

我的 QuirkyListAdapter 方法将 JSON null""false 正确反序列化为一个空列表。

public class QuirkyListAdapter<T> extends TypeAdapter<List<T>> 
  @Override
  public void write( @NonNull JsonWriter out, List<T> listOfT ) throws IOException 
    final Gson gson = new Gson();
    final TypeAdapter<T> typeAdapter = gson.getAdapter( T.class );  // Note: No legal Java syntax
    out.beginArray();
    if( listOfT != null ) 
      for( final T val : listOfT ) 
        typeAdapter.write( out, val );
      
    
    out.endArray();
  

  @Override
  @NonNull
  public List<T> read( @NonNull JsonReader in ) throws IOException 
    final List<T> result = new ArrayList<>();
    final JsonToken peekedToken = in.peek();

    if( peekedToken == JsonToken.NULL ) 
      in.nextNull();
      return result;
    
    if( peekedToken == JsonToken.BOOLEAN ) 
      if( in.nextBoolean() )
        throw new IllegalStateException( "Expected an empty array encoded as boolean value \"FALSE\" but found \"true\"" );
      return result;
    
    if( peekedToken == JsonToken.STRING ) 
      final String str = in.nextString();
      if( str.equals( "" ) )
        throw new IllegalStateException( "Expected an empty array encoded as an empty string value but found \"" + str + "\"" );
      return result;
    
    if( peekedToken == JsonToken.BEGIN_OBJECT ) 
      in.beginObject();
      if( in.peek() != JsonToken.END_OBJECT)
        throw new IllegalStateException( "Expected an empty array encoded as an empty object \"\", but object has attributes" );
      in.endObject(); 
    
    if( peekedToken != JsonToken.BEGIN_ARRAY )
      throw new IllegalStateException( "Expected BEGIN_ARRAY but was " + peekedToken );

    final Gson gson = new Gson();
    final TypeAdapter<T> typeAdapter = gson.getAdapter( T.class );  // Note: No legal Java syntax

    in.beginArray();
    while( in.hasNext() ) 
      result.add( typeAdapter.read( in ) );
    
    in.endArray();
    return result;
  

如果我将通用的T 替换为FooBar,并实现两个独立的QuirkyFooListAdapterQuirkyBarListAdapter,一切都按预期进行。 即使用Object 替换T 也不是解决方案,尽管它可以编译。 然而,在我古怪的列表适配器中,棘手的行是gson.getAdapter( .. )。 使用gson.getAdapter( Object.class ) 显然不会返回反序列化列表中正确对象所需的适配器。

【问题讨论】:

“这就是我犹豫要不要为 GSON 编写可能自己的适配器的方式,因为我担心这个列表很快就会变得无穷无尽。” -- 这听起来像是一种虚假的恐惧: PHP 方面有一组有限的怪癖,您提供的列表似乎非常完整。 “是否有任何库已经为那些典型的 PHP 问题实现了转换?” -- 我怀疑它们是否存在,因为它们无法处理所有古怪情况的所有可能情况,因此没有理由“支持” " 全球范围内的怪癖,但值得将它们本地化。 只需创建类型适配器来处理您提到的情况。即使类型适配器因以前未知的情况而失败,您也会得到一个 JSON 解析异常,允许您添加另一个情况(希望它不会导致任何问题)。我认为这是合理的,并且可能是您情况下唯一可行的解​​决方案。 【参考方案1】:

您不需要那么多类型适配器,因为您可以将常见的反序列化逻辑合并到为从该后端遇到的每种有问题的文字类型(就像您在问题中发布的那样)设计的类型适配器中。

类型适配器是独立创建的,不是在类型适配器工厂中创建的,通常适合简单的情况。工厂提供对共享 context Gson 实例(您配置然后使用的实例)的访问,并提供一个具体类型来构建类型适配器(这是“T.class”可以使用的地方周围)。

public abstract class AbstractQuirkyTypeAdapterFactory<T, C>
        implements TypeAdapterFactory 

    protected abstract boolean supports(@Nonnull TypeToken<?> typeToken);

    @Nullable
    protected abstract C createContext(@Nonnull TypeToken<?> typeToken);

    @Nullable
    protected abstract T read(@Nullable C context, @Nonnull TypeAdapter<? extends T> delegateAdapter, @Nonnull JsonReader in)
            throws IOException;

    @Override
    @Nullable
    public final <U> TypeAdapter<U> create(final Gson gson, final TypeToken<U> typeToken) 
        if ( !supports(typeToken) ) 
            return null;
        
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> delegateAdapter = (TypeAdapter<T>) gson.getDelegateAdapter(this, typeToken);
        @Nullable
        final C context = createContext(typeToken);
        final TypeAdapter<T> quirkyAdapter = new TypeAdapter<T>() 
            @Override
            public void write(final JsonWriter out, final T value)
                    throws IOException 
                delegateAdapter.write(out, value);
            

            @Override
            public T read(final JsonReader in)
                    throws IOException 
                return AbstractQuirkyTypeAdapterFactory.this.read(context, delegateAdapter, in);
            
        
                .nullSafe();
        @SuppressWarnings("unchecked")
        final TypeAdapter<U> typeAdapter = (TypeAdapter<U>) quirkyAdapter;
        return typeAdapter;
    


public final class QuirkyBooleanTypeAdapterFactory
        extends AbstractQuirkyTypeAdapterFactory<Boolean, Void> 

    private static final TypeAdapterFactory instance = new QuirkyBooleanTypeAdapterFactory();

    private QuirkyBooleanTypeAdapterFactory() 
    

    public static TypeAdapterFactory getInstance() 
        return instance;
    

    @Override
    protected boolean supports(@Nonnull final TypeToken<?> typeToken) 
        final Class<?> rawType = typeToken.getRawType();
        return rawType == boolean.class
                || rawType == Boolean.class;
    

    @Nullable
    @Override
    protected Void createContext(@Nonnull final TypeToken<?> typeToken) 
        return null;
    

    @Override
    @Nullable
    @SuppressWarnings("NestedSwitchStatement")
    protected Boolean read(@Nullable final Void context, @Nonnull final TypeAdapter<? extends Boolean> delegateAdapter, @Nonnull final JsonReader in)
            throws IOException 
        final JsonToken token = in.peek();
        switch ( token ) 
        case BOOLEAN:
            return delegateAdapter.read(in);
        case NUMBER:
            final int i = in.nextInt();
            switch ( i ) 
            case 0:
                return false;
            case 1:
                return true;
            default:
                throw new JsonSyntaxException("Unhandled integer: " + i);
            
        case STRING:
            final String s = in.nextString();
            switch ( s ) 
            case "0":
            case "false":
            case "null":
                return false;
            case "1":
            case "true":
                return true;
            default:
                throw new JsonSyntaxException("Unhandled string: " + s);
            
        case NULL:
            return null; // TODO or false?
        case BEGIN_ARRAY:
        case END_ARRAY:
        case BEGIN_OBJECT:
        case END_OBJECT:
        case NAME:
        case END_DOCUMENT:
            throw new JsonSyntaxException("Unhandled token: " + token);
        default:
            throw new AssertionError(token);
        
    


public final class QuirkyNumberTypeAdapterFactory
        extends AbstractQuirkyTypeAdapterFactory<Number, Number> 

    private static final TypeAdapterFactory instance = new QuirkyNumberTypeAdapterFactory();

    private static final Function<Class<?>, Number> getKnownZero = new ImmutableMap.Builder<Class<?>, Number>()
            .put(byte.class, (byte) 0)
            .put(Byte.class, (byte) 0)
            .put(short.class, (short) 0)
            .put(Short.class, (short) 0)
            .put(int.class, 0)
            .put(Integer.class, 0)
            .put(long.class, 0L)
            .put(Long.class, 0L)
            .put(float.class, 0F)
            .put(Float.class, 0F)
            .put(double.class, 0D)
            .put(Double.class, 0D)
            .put(BigInteger.class, BigInteger.ZERO)
            .put(BigDecimal.class, BigDecimal.ZERO)
            .build()
            ::get;

    private QuirkyNumberTypeAdapterFactory() 
    

    public static TypeAdapterFactory getInstance() 
        return instance;
    

    @Override
    @SuppressWarnings("OverlyComplexBooleanExpression")
    protected boolean supports(@Nonnull final TypeToken<?> typeToken) 
        final Class<?> rawType = typeToken.getRawType();
        return Number.class.isAssignableFrom(rawType)
                || rawType == byte.class
                || rawType == short.class
                || rawType == int.class
                || rawType == long.class
                || rawType == float.class
                || rawType == double.class;
    

    @Nullable
    @Override
    protected Number createContext(@Nonnull final TypeToken<?> typeToken) 
        return getKnownZero.apply(typeToken.getRawType());
    

    @Override
    @Nullable
    @SuppressWarnings("NestedSwitchStatement")
    protected Number read(@Nullable final Number knownZero, @Nonnull final TypeAdapter<? extends Number> delegateAdapter, @Nonnull final JsonReader in)
            throws IOException 
        final JsonToken token = in.peek();
        switch ( token ) 
        case NUMBER:
            return delegateAdapter.read(in);
        case STRING:
            final String s = in.nextString();
            switch ( s ) 
            case "null":
            case "false":
                if ( knownZero == null ) 
                    return delegateAdapter.read(new JsonReader(new StringReader("0"))); // TODO optimize "constant" reading or cache previously unknown zero
                
                return knownZero;
            default:
                return delegateAdapter.fromJsonTree(new JsonPrimitive(s)); // TODO optimize bypassing the intermediate JSON element
            
        case BOOLEAN:
            final boolean b = in.nextBoolean();
            if ( !b ) 
                if ( knownZero == null ) 
                    return delegateAdapter.read(new JsonReader(new StringReader("0"))); // TODO optimize "constant" reading or cache previously unknown zero
                
                return knownZero;
            
            throw new JsonSyntaxException("Unhandled boolean: " + b);
        case NULL:
            return null; // TODO or zero?
        case BEGIN_ARRAY:
        case END_ARRAY:
        case BEGIN_OBJECT:
        case END_OBJECT:
        case NAME:
        case END_DOCUMENT:
            throw new JsonSyntaxException("Unhandled token: " + token);
        default:
            throw new AssertionError(token);
        
    


public final class QuirkyCollectionTypeAdapterFactory
        extends AbstractQuirkyTypeAdapterFactory<Collection<?>, Void> 

    private static final TypeAdapterFactory instance = new QuirkyCollectionTypeAdapterFactory();

    private QuirkyCollectionTypeAdapterFactory() 
    

    public static TypeAdapterFactory getInstance() 
        return instance;
    

    @Override
    protected boolean supports(@Nonnull final TypeToken<?> typeToken) 
        return Collection.class.isAssignableFrom(typeToken.getRawType());
    

    @Nullable
    @Override
    protected Void createContext(@Nonnull final TypeToken<?> typeToken) 
        return null;
    

    @Override
    @Nullable
    protected Collection<?> read(@Nullable final Void context, @Nonnull final TypeAdapter<? extends Collection<?>> delegateAdapter,
            @Nonnull final JsonReader in)
            throws IOException 
        final JsonToken token = in.peek();
        switch ( token ) 
        case BEGIN_ARRAY:
            return delegateAdapter.read(in);
        case BOOLEAN:
            final boolean b = in.nextBoolean();
            if ( !b ) 
                return delegateAdapter.read(new JsonReader(new StringReader("[]"))); // TODO optimize "constant" reading (caching is not possible: collections are supposed be new and mutable)
            
            throw new JsonSyntaxException("Unhandled boolean: " + b);
        case NULL:
            return null; // TODO or empty collection?
        case END_ARRAY:
        case BEGIN_OBJECT:
        case END_OBJECT:
        case NAME:
        case STRING:
        case NUMBER:
        case END_DOCUMENT:
            throw new JsonSyntaxException("Unhandled token: " + token);
        default:
            throw new AssertionError(token);
        
    


上述方法为所有三种单独的情况实现了模板方法设计模式:布尔值、数字和数组(JSON 数组,但 Java 集合,为简洁起见,我没有包括 Java 数组适配器)。

它们背后的共享逻辑如下:

类型适配器工厂检查它是否可以处理给定的类型。 如果可以,则向 Gson 请求委托类型适配器,然后处理。 创建一个泛型类型适配器,它只是将写入操作委托给原始类型适配器,但读取操作专门用于访问它的每个子类。 每个读取操作都会实现简单的 JSON 令牌窥视,以根据您在问题中描述的怪癖来决定如何进一步进行。

具有以下 JSON:


    "booleans": [
        true,
        false,
        0,
        1,
        "0",
        "1",
        "true",
        "false",
        null,
        "null"
    ],
    "numbers": [
        42,
        "42",
        null,
        "null",
        false,
        "false"
    ],
    "arrays": [
        [
            "foo",
            "bar"
        ],
        [],
        false,
        null
    ]

以下测试通过:

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
final class Data 

    @SerializedName("booleans")
    final List<Boolean> booleans;

    @SerializedName("numbers")
    final List<Number> numbers;

    @SerializedName("arrays")
    final List<List<String>> arrays;


public final class QuirksTest 

    private static final Gson gson = new GsonBuilder()
            .disablehtmlEscaping()
            .disableInnerClassSerialization()
            .registerTypeAdapterFactory(QuirkyBooleanTypeAdapterFactory.getInstance())
            .registerTypeAdapterFactory(QuirkyNumberTypeAdapterFactory.getInstance())
            .registerTypeAdapterFactory(QuirkyCollectionTypeAdapterFactory.getInstance())
            .create();

    @Test
    @SuppressWarnings("ReturnOfNull")
    public void test()
            throws IOException 
        try ( final JsonReader jsonReader = open("quirks.json") ) 
            final Data data = gson.fromJson(jsonReader, Data.class);
            Assertions.assertIterableEquals(
                    Arrays.asList(true, false, false, true, false, true, true, false, null, false),
                    data.booleans
            );
            Assertions.assertIterableEquals(
                    Arrays.asList(42, 42, null, 0, 0, 0),
                    data.numbers
                            .stream()
                            .map(n -> n != null ? n.intValue() : null)
                            .collect(Collectors.toList())
            );
            Assertions.assertIterableEquals(
                    Arrays.asList(
                            Arrays.asList("foo", "bar"),
                            Collections.emptyList(),
                            Collections.emptyList(),
                            null
                    ),
                    data.arrays
            );
        
    


【讨论】:

以上是关于如何使用 Retrofit2 和 GSON 转换器解决“古怪”的 JSON API的主要内容,如果未能解决你的问题,请参考以下文章

如何使用Retrofit2,RxJava2,Gson TypeAdapterFActory正确映射Gson?

改造:来自 gson 转换体的原始响应体

如何在对象 Gson/Retrofit2 中获取字符串

ProGuard for Android 和 Retrofit2 Converter Gson?

使用 DEFAULT_SERIALIZE_NULLS 值时不会跳过 Retrofit2/Gson 空值

Android实战——Retrofit2的使用和封装