如何使用 Retrofit2 处理嵌套 JsonObjects 的动态字段

Posted

技术标签:

【中文标题】如何使用 Retrofit2 处理嵌套 JsonObjects 的动态字段【英文标题】:How to handle dynamic fields of nested JsonObjects using Retrofit2 【发布时间】:2020-04-01 14:17:22 【问题描述】:

最近在制作一个使用 Rest API 的项目时,我决定使用 Retrofit。然而,在向所述 API 发送 GET 请求时,我遇到了一个问题,尽管有人提出了类似的问题,但我觉得无法找到解决方案。

我遇到的问题(我相信这就是问题所在)是 API 给出的响应会根据给定的输入而变化,因此我的 POJO 并不总是匹配。更具体地说,我有时会得到一个对象而不是列表作为字段。这一行很好地描述了错误的要点:

Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 250 column 20 path $.LocationList.CoordLocation

从 API 收到的 Json 大部分时间看起来像这样:


"LocationList": 
  "StopLocation": [
    
      "name": String,
      "x": String,
      "y": String,
      "id": String
    ,
...
   ],
   "CoordLocation": [
     
      "name": String,
      "x": String,
      "y": String,
      "type": String
     ,
...
    ]
  
 

这一切都很好,因为我的 LocationList POJO 被定义为具有 CoordLocations 和 StopLocations 的列表。但是,当某些输入的 API 仅响应 CoordLocation 的单个 StopLocation 时,由于收到的 Json 是单个对象而不是具有单个对象条目的数组,因此一切都会崩溃:

    
"LocationList": 
  "StopLocation": [
    
      "name": String,
      "x": String,
      "y": String,
      "id": String
    ,
...
   ],
  "CoordLocation": 
      "name": String,
      "x": String,
      "y": String,
      "type": String
  
 

所以我的问题是如何解决这个问题?到目前为止,我已经看到有人建议自定义 TypeAdapters 和 Deserializers,但每次我尝试遵循解决方案时,我都会在某个时候碰壁,因为情况略有不同,而且我对所有与 API 相关的东西都很陌生。

【问题讨论】:

作为最佳实践,CoordLocation 必须始终具有服务器发送的对象数组。在应用程序端,检查大小是否为 1,以便执行必要的操作。 我很清楚这是服务器应该如何响应的,但遗憾的是它是一个第 3 方 API,因此更改响应是不可能的。 【参考方案1】:

在这种情况下,我建议您使用JsonElement 作为您的回复。当你得到响应时检查它是JsonObject还是JsonArray,然后将响应对象转换为pojo或其他东西。

【讨论】:

我不太确定这将如何工作,因为它是响应的两个字段,它们是单个对象或列表,而不是响应本身。我想我可以手动反序列化 onResponse 中的 JsonElement,但是我将如何去做呢? 如果我理解你的问题是你的 API 的响应不稳定,它可以是arrayobject。对于这种情况,您可以设置为您的 api JsonElement 的响应。当您收到回复时,您可以检查它是isJsonArray()isJsonPrimitive 还是isJsonObject()(JsonElement 具有此功能)。检查后,您可以使用 Gson YourModel model = new Gson().fromJson(JsonElement, YourModel.class) 将您的回复投射到 List 或 Object。【参考方案2】:

结果证明自定义 TypeAdapter 是可行的方法,而我只是没有足够的经验来编写自己的前几次尝试。但我终于成功了,结果是这样的:

package com.example.android_navigation.RejseplanenAPI;

import com.example.android_navigation.RejseplanenAPI.POJOs.CoordLocation;
import com.example.android_navigation.RejseplanenAPI.POJOs.Location;
import com.example.android_navigation.RejseplanenAPI.POJOs.StopLocation;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class LocationTypeAdapter extends TypeAdapter<Location> 

@Override
public Location read(JsonReader reader) throws IOException 
    Location location = new Location();
    Gson gson = new Gson();

    JsonParser parser = new JsonParser();
    JsonObject jsonObject = (JsonObject)parser.parse(reader);
    JsonObject locationListObject = jsonObject.get("LocationList").getAsJsonObject();
    JsonElement stopLocations = locationListObject.get("StopLocation");
    JsonElement coordLocations = locationListObject.get("CoordLocation");

    if(stopLocations != null) 
        if(stopLocations.isJsonArray())
            StopLocation[] stopArray = gson.fromJson(stopLocations, StopLocation[].class);
            location.getLocationList().setStopLocations(Arrays.asList(stopArray));
        
        else if (stopLocations.isJsonObject()) 
            StopLocation stopLocation = gson.fromJson(stopLocations, StopLocation.class);
            List<StopLocation> stopLocationsList = new ArrayList<>();
            stopLocationsList.add(stopLocation);
            location.getLocationList().setStopLocations(stopLocationsList);
        
    
    if(coordLocations != null) 
        if (coordLocations.isJsonArray()) 
            CoordLocation[] coordArray = gson.fromJson(coordLocations, CoordLocation[].class);
            location.getLocationList().setCoordLocations(Arrays.asList(coordArray));
         else if (coordLocations.isJsonObject()) 
            CoordLocation coordLocation = gson.fromJson(coordLocations, CoordLocation.class);
            List<CoordLocation> coordLocationsList = new ArrayList<>();
            coordLocationsList.add(coordLocation);
            location.getLocationList().setCoordLocations(coordLocationsList);
        
    
    return location;


@Override
public void write(JsonWriter out, Location value) throws IOException 



然后在构建 Retrofit 实例中使用的 Gson 时添加它:

    private Gson gson = new GsonBuilder()
        .registerTypeAdapter(Location.class, new LocationTypeAdapter())
        .setLenient()
        .create();
    private Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://xmlopen.rejseplanen.dk/bin/rest.exe/")
        .addConverterFactory(GsonConverterFactory.create(gson))
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .build();

【讨论】:

以上是关于如何使用 Retrofit2 处理嵌套 JsonObjects 的动态字段的主要内容,如果未能解决你的问题,请参考以下文章

使用 MockWebServer 模拟嵌套的改造 api 调用

使用 MockWebServer 模拟嵌套的改造 api 调用

如何使用 Retrofit 2.0 (Kotlin) 正确解析嵌套的 JSON 对象?

Retrofit2.0+RxJava网络请求异常统一处理

如何使用 Retrofit 2 处理空响应体?

预期 BEGIN_OBJECT 但在路径 $ 处是 BEGIN_ARRAY - 使用 RxJava 处理 JSONArray 响应的 Retrofit2