Expected a string but was BEGIN_OBJECT

问题背景

针对一个接口,server给出的是一个JSON, 端上想要用string去接收

我们测试可以用https://jsonplaceholder.typicode.com/posts/3来测试

1
2
3
4
5
6
{
"userId": 1,
"id": 3,
"title": "title",
"body": "body"
}

我们是不是能用直接用String来接收?

1
2
3
4
interface ApiService {
@GET("/posts/33")
fun getPage(): Single<String>
}

问题分析

这里我们可以会想都是字符串,肯定是可以的,但是转念一下,JSON和JSON字符串是不一样的,
这个是JSON

1
2
3
4
5
6
{
"userId": 1,
"id": 3,
"title": "title",
"body": "body"
}

这是JSON字符串 “{"userId":1,"id":3,"title":"title","body":"body"}”
我们直接来测试, 会得到这样的错误❎, 大意是期望的是一个String,但是得到是一个JSON类型
Expected a string but was BEGIN_OBJECT at line 1 column 2 path $
当我们调用一个interface的api的时候,通过注解登拿到这个方法的返回类型,然后去找Response Body Converter

1
2
3
4
5
6
7
8
9
10
<init>:33, GsonResponseBodyConverter (retrofit2.converter.gson)
responseBodyConverter:65, GsonConverterFactory (retrofit2.converter.gson)
nextResponseBodyConverter:362, Retrofit (retrofit2)
responseBodyConverter:345, Retrofit (retrofit2)
createResponseConverter:124, HttpServiceMethod (retrofit2)
parseAnnotations:85, HttpServiceMethod (retrofit2)
parseAnnotations:39, ServiceMethod (retrofit2)
loadServiceMethod:202, Retrofit (retrofit2)
invoke:160, Retrofit$1 (retrofit2)
getPage:-1, $Proxy0 (jdk.proxy1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
// 其实就是去converterFactories里面找
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<ResponseBody, T>) converter;
}
}
//
}

走到在我们注册的GsonConverterFactory里面的responseBodyConverter实现里面

1
2
3
4
5
6
7
8
9
10
11
12
  public static GsonConverterFactory create(Gson gson) {

//
@Override
public Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
// 去找具体的转换类型,例如本案例就是 type就是 java.lang.String
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
}
//
}

最终走到com.google.gson.Gson#getAdapter(com.google.gson.reflect.TypeToken) 获取到一个TypeAdapter, 其实在Gson()的构造方法中,一些基本类型和内置类型,提前加到了factories里面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
TypeAdapter<?> cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type);
//
try {
FutureTypeAdapter<T> call = new FutureTypeAdapter<T>();
threadCalls.put(type, call);

for (TypeAdapterFactory factory : factories) {
// 找一个能处理String的TypeAdapterFactory
TypeAdapter<T> candidate = factory.create(this, type);
// 这里是基本类型,返回了TypeAdapters
if (candidate != null) {
call.setDelegate(candidate);
typeTokenCache.put(type, candidate);
return candidate;
}
}
//
}
1
2
3
4
/**
* Type adapters for basic types.
*/
public final class TypeAdapters

TypeAdapters 可以来处理基本类型解析
Response的解析,从retrofit2.OkHttpCall#parseResponse的开始会走到

  • retrofit2.converter.gson.GsonResponseBodyConverter#convert

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public T convert(ResponseBody value) throws IOException {
    JsonReader jsonReader = gson.newJsonReader(value.charStream());
    try {
    // 这个adapter就是前面找到的TypeAdapters
    T result = adapter.read(jsonReader);
    if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
    throw new JsonIOException("JSON document was not fully consumed.");
    }
    return result;
    } finally {
    value.close();
    }
    }

    接下来就是由TypeAdapters去解析成想要的String类型,
    会走到STRING的read中,由于这个是string类型read,就确定需要读出一个string,继续走到nextString中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
    @Override
    public String read(JsonReader in) throws IOException {
    // 这里peek,可以判断出一个json类型
    JsonToken peek = in.peek();
    if (peek == JsonToken.NULL) {
    in.nextNull();
    return null;
    }
    /* coerce booleans to strings for backwards compatibility */
    if (peek == JsonToken.BOOLEAN) {
    return Boolean.toString(in.nextBoolean());
    }
    return in.nextString();
    }
    //
    };
  • com.google.gson.stream.JsonReader#nextString

在前面的peek中peeked等于了 PEEKED_BEGIN_OBJECT,表面当前是一个BEGIN_OBJECT标识

1
2
case '{':
return peeked = PEEKED_BEGIN_OBJECT;

在 nextString()中,发现p执行的所有case中,没有匹配的BEGIN_OBJECT,于是抛错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public String nextString() throws IOException {
// 当前的指针,当前的指针指向的是 PEEKED_BEGIN_OBJECT
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
String result;
// 如果是这些基本类,这里实际上进行了toString的简单转换
if (p == PEEKED_UNQUOTED) {
result = nextUnquotedValue();
} else if (p == PEEKED_SINGLE_QUOTED) {
result = nextQuotedValue('\'');
} else if (p == PEEKED_DOUBLE_QUOTED) {
result = nextQuotedValue('"');
} else if (p == PEEKED_BUFFERED) {
result = peekedString;
peekedString = null;
} else if (p == PEEKED_LONG) {
result = Long.toString(peekedLong);
} else if (p == PEEKED_NUMBER) {
result = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
} else {
throw new IllegalStateException("Expected a string but was " + peek() + locationString());
}
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
}

想要解决这个问题,就是找到正确的TypeAdapter,要么更改类型,要么添加新的TypeAdapter处理string

解决方法

用内置类型

  • server直接返回string字符串类型

  • 端上换个接收类型,可以用gson包的 com.google.gson.JsonElement 或者 okhttp包里面的ResponseBody

    • 因为JsonElement的TypeAdapter也在Gson里面默认构造好了factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);和String相比就是它的表示类型不一样
    • 因为ResponseBody对应的retrofit2.BuiltInConverters.BufferingResponseBodyConverter在retrofit包里内置了
  • 自定义一个TypeAdapter

    自定义TypeAdapter

    我测试用的 ‘com.google.code.gson:gson:2.9.0’, 通过GsonBuilder来注册一个CustomStringTypeAdapter

  • CustomStringTypeAdapter

    1
    2
    3
    4
    5
    6
    7
    8
    class CustomStringTypeAdapter : TypeAdapter<String>() {
    // ....
    @Throws(IOException::class)
    override fun read(`in`: JsonReader): String? {
    // 处理读取JsonReader里面内容,
    return context
    }
    }

    注册使用

    1
    2
    3
    val gson: Gson = GsonBuilder()
    .registerTypeAdapter(String::class.java, CustomStringTypeAdapter())
    .create()
  • com.google.gson.Gson#Gson

    在Gson的构造函数中,会提前加到factories中,这样保证了优先级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // .....
    // users' type adapters 我们自定义的CustomStringTypeAdapter
    factories.addAll(factoriesToBeAdded);

    // type adapters for basic platform types
    factories.add(TypeAdapters.STRING_FACTORY);
    factories.add(TypeAdapters.INTEGER_FACTORY);
    factories.add(TypeAdapters.BOOLEAN_FACTORY);
    // .....