2017-06-06 63 views
1

是否有可能實現以下兩個目標?使用Gson TypeAdapter以排序不敏感的方式反序列化JSON對象

  • 能夠委託給調用我們的自定義實現的默認Gson解串器。
  • 是由不同的順序按鍵在JSON對象

下面我介紹,只有實現這些目標的一個兩種可能的方法不受影響。


我現在不是一個成功的喜歡與回報工作的API:

{ 
    "type": "success", 
    "data": { 
    "display_name": "Invisible Pink Unicorn", 
    "user_email": "[email protected]", 
    "user_id": 1234 
    } 
} 

或錯誤,如:

{ 
    "type": "error", 
    "data": { 
     "error_name": "incorrect_password", 
     "error_message": "The username or password you entered is incorrect." 
    } 
} 

它目前的處理方式是通過註冊如果類型爲"error"則拋出異常並帶有給定"error_message"的TypeAdapter:

new GsonBuilder() 
    .registerTypeAdapter(User.class, new ContentDeserializer<User>()) 
    .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 
    .create() 

public class ContentDeserializer<T> implements JsonDeserializer<T> { 
    @Override 
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 
     final JsonObject object = json.getAsJsonObject(); 
     final String type = object.get("type").getAsString(); 
     final JsonElement data = object.get("data"); 
     final Gson gson = new Gson(); 
     if ("error".equals(type)) { 
      throw gson.fromJson(data, ApiError.class); 
     } else { 
      return gson.fromJson(data, typeOfT); 
     } 
    } 
} 

它很整潔,因爲它非常簡潔,並且使用默認的解串器來完成所有的辛苦工作。

但實際上這是錯誤的,因爲它不使用相同的Gson委託該工作,所以它會使用不同的字段命名策略。

爲了解決這個問題,我寫了一個TypeAdapterFactory:

public class UserAdapterFactory implements TypeAdapterFactory { 

    @SuppressWarnings("unchecked") 
    @Override 
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { 
     if (!User.class.isAssignableFrom(type.getRawType())) return null; 
     final TypeAdapter<User> userAdapter = (TypeAdapter<User>) gson.getDelegateAdapter(this, type); 
     final TypeAdapter<ApiError> apiErrorAdapter = gson.getAdapter(ApiError.class); 
     return (TypeAdapter<T>) new Adapter(userAdapter, apiErrorAdapter); 
    } 

    private static class Adapter extends TypeAdapter<User> { 
     private final TypeAdapter<User> userAdapter; 
     private final TypeAdapter<ApiError> apiErrorAdapter; 

     Adapter(TypeAdapter<User> userAdapter, TypeAdapter<ApiError> apiErrorAdapter) { 
      this.userAdapter = userAdapter; 
      this.apiErrorAdapter = apiErrorAdapter; 
     } 

     @Override 
     public void write(JsonWriter out, User value) throws IOException { 
     } 

     @Override 
     public User read(JsonReader in) throws IOException { 
      User user = null; 
      String type = null; 
      in.beginObject(); 
      while (in.hasNext()) { 
       switch (in.nextName()) { 
        case "type": 
         type = in.nextString(); 
         break; 
        case "data": 
         if ("error".equals(type)) { 
          throw apiErrorAdapter.read(in); 
         } else if ("success".equals(type)) { 
          user = userAdapter.read(in); 
         } 
         break; 
       } 
      } 
      in.endObject(); 
      return user; 
     } 
    } 
} 

這是一個很多工作,但至少讓我委託給同GSON配置。

這種方法的問題在於,它打破時,JSON對象將有不同的順序:

{ 
    "data": { 
    "display_name": "Invisible Pink Unicorn", 
    "user_email": "[email protected]", 
    "user_id": 1234 
    }, 
    "type": "success" 
} 

而且我沒有看到解決這個辦法,因爲我不認爲JsonReader有一個選項可以讀取輸入兩次,也沒有辦法將「數據」值緩存在抽象類型中,例如JsonElement以在遇到「類型」後解析。

+0

爲什麼不反序列化'data'到'地圖'? – syntagma

+0

'context.deserialize(data,typeOfT);'不工作? –

+0

@syntagma「user_id」不是一個字符串,對於初學者來說。但即使如果「type」是「錯誤」,那麼如何幫助我拋出'ApiError',否則返回'User'? – arekolek

回答

1

But actually it's wrong, because it doesn't use the same Gson to delegate that work to, so it will use a different field naming policy, for example.

正確。您應該使用JsonDeserializationContext

... because I don't think JsonReader has an option to read the input twice, there's also no way to cache the "data" value in an abstract type like JsonElement to parse after "type" has been encountered.

正確。 JsonReader是一個流媒體閱讀器,而JsonElement是一棵樹。這些就像XML世界中的SAX和DOM一樣,各有利弊。流式讀取器只是讀取輸入流,而您必須自己緩衝/緩存中間數據。

可以使用這兩種方法您的情況,但我會去JsonDeserializer爲簡單(假設你不會寫一個超快速解串器)。

我不太確定您的UserApiError是如何相互關聯的,但我會爲兩種不同類型的值使用普通類:真實值和錯誤。它看起來像你的兩個班有一個共同的父母或祖先,但我真的不知道你是如何處理他們在呼叫現場(也許instanceof?)。再說,這樣的事情(構造函數隱藏,以封裝對象初始化結構的複雜性):

final class Content<T> { 

    private final boolean isSuccess; 
    private final T data; 
    private final ApiError error; 

    private Content(final boolean isSuccess, final T data, final ApiError error) { 
     this.isSuccess = isSuccess; 
     this.data = data; 
     this.error = error; 
    } 

    static <T> Content<T> success(final T data) { 
     return new Content<>(true, data, null); 
    } 

    static <T> Content<T> error(final ApiError error) { 
     return new Content<>(false, null, error); 
    } 

    boolean isSuccess() { 
     return isSuccess; 
    } 

    T getData() 
      throws IllegalStateException { 
     if (!isSuccess) { 
      throw new IllegalStateException(); 
     } 
     return data; 
    } 

    ApiError getError() 
      throws IllegalStateException { 
     if (isSuccess) { 
      throw new IllegalStateException(); 
     } 
     return error; 
    } 

} 

從我的角度來看這兩個UserApiError(我喜歡@SerializedName雖然有超過命名更強的控制 - 但它似乎是一個習慣問題)。

final class ApiError { 

    @SuppressWarnings("error_name") 
    final String errorName = null; 

    @SerializedName("error_message") 
    final String errorMessage = null; 

} 
final class User { 

    @SerializedName("display_name") 
    final String displayName = null; 

    @SerializedName("user_email") 
    final String userEmail = null; 

    @SuppressWarnings("user_id") 
    final int userId = Integer.valueOf(0); 

} 

接下來,由於樹的操作更容易,只是實現你的JSON解串器:

final class ContentJsonDeserializer<T> 
     implements JsonDeserializer<Content<T>> { 

    // This deserializer holds no state 
    private static final JsonDeserializer<?> contentJsonDeserializer = new ContentJsonDeserializer<>(); 

    private ContentJsonDeserializer() { 
    } 

    // ... and we hide away that fact not letting this one to be instantiated at call sites 
    static <T> JsonDeserializer<T> getContentJsonDeserializer() { 
     // Narrowing down the @SuppressWarnings scope -- suppressing warnings for entire method may be harmful 
     @SuppressWarnings("unchecked") 
     final JsonDeserializer<T> contentJsonDeserializer = (JsonDeserializer<T>) ContentJsonDeserializer.contentJsonDeserializer; 
     return contentJsonDeserializer; 
    } 

    @Override 
    public Content<T> deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context) 
      throws JsonParseException { 
     final JsonObject jsonObject = jsonElement.getAsJsonObject(); 
     final String responseType = jsonObject.getAsJsonPrimitive("type").getAsString(); 
     switch (responseType) { 
     case "success": 
      return success(context.deserialize(jsonObject.get("data"), getTypeParameter0(type))); 
     case "error": 
      return error(context.deserialize(jsonObject.get("data"), ApiError.class)); 
     default: 
      throw new JsonParseException(responseType); 
     } 
    } 

    // Trying to detect any given type parameterization for its first type parameter 
    private static Type getTypeParameter0(final Type type) { 
     if (!(type instanceof ParameterizedType)) { 
      return Object.class; 
     } 
     return ((ParameterizedType) type).getActualTypeArguments()[0]; 
    } 

} 

演示:

private static final Gson gson = new GsonBuilder() 
     .registerTypeAdapter(Content.class, getContentJsonDeserializer()) 
     .create(); 

private static final Type userContent = new TypeToken<Content<User>>() { 
}.getType(); 

public static void main(final String... args) 
     throws IOException { 
    for (final String name : ImmutableList.of("success.json", "error.json", "success-reversed.json", "error-reversed.json")) { 
     try (final JsonReader jsonReader = getPackageResourceJsonReader(Q44400163.class, name)) { 
      final Content<User> content = gson.fromJson(jsonReader, userContent); 
      if (content.isSuccess()) { 
       System.out.println("SUCCESS: " + content.getData().displayName); 
      } else { 
       System.out.println("ERROR: " + content.getError().errorMessage); 
      } 
     } 
    } 
} 

輸出:

SUCCESS: Invisible Pink Unicorn
ERROR: The username or password you entered is incorrect.
SUCCESS: Invisible Pink Unicorn
ERROR: The username or password you entered is incorrect.

現在,回到你原來的問題TypeAdapter。正如我上面提到的,你可以使用一個類型的適配器以及做,但你必須實現兩個案例支持:

  • 前進的情況下,你已經實現了它(最好的情況下):讀取type財產第一,然後根據您的真實日期類型閱讀data財產。順便說一句,您的TypeAdapter實現遠不是通用的:您必須使用Gson.getDelegateAdapter來解析實際的數據類型及其適配器。
  • 相反的情況(在最壞的情況下):讀data財產成樹視圖(因此緩存到內存)爲JsonElement實例(你必須從Gson實例得到TypeAdapter<JsonElement>create方法首先),然後,根據下一個type屬性值,使用TypeAdapter.fromJsonTree將其作爲樹中的值讀取。

是的,不要忘記檢查一下這裏的解析狀態(處理缺少這兩種情況下typedata不知)。如您所見,這會引入可變的複雜性和性能/內存成本,但它可以爲您提供最佳性能。你決定。