-
Notifications
You must be signed in to change notification settings - Fork 136
Deserializer for unexpected array format from endpoints 2 #14653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deserializer for unexpected array format from endpoints 2 #14653
Conversation
This commit introduces a new Gson deserializer, `ArrayOrObjectDeserializer`.
📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
|
|
📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #14653 +/- ##
=========================================
Coverage 38.52% 38.52%
+ Complexity 9783 9780 -3
=========================================
Files 2068 2068
Lines 115558 115567 +9
Branches 15394 15394
=========================================
+ Hits 44522 44526 +4
- Misses 66899 66902 +3
- Partials 4137 4139 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
hichamboushaba
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @irfano, tests work as expected, and ready to ship.
I'm just leaving an optional suggestion for you, I think we can simplify the code a bit and reduce the code repetition between this new component and the existing ListOrObjectDeserializer, please check the following patch:
Index: libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ArrayOrObjectDeserializer.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ArrayOrObjectDeserializer.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ArrayOrObjectDeserializer.kt
--- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ArrayOrObjectDeserializer.kt (revision 88217ddda7dd0897c76a173d49272a1e6133797e)
+++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ArrayOrObjectDeserializer.kt (date 1758795060680)
@@ -4,9 +4,11 @@
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
+import com.google.gson.internal.GsonTypes.getRawType
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
+import java.lang.reflect.Array as JArray
/**
* Deserializes a JSON value that can be either an array or an object with numeric string keys.
@@ -27,36 +29,68 @@
typeOfT: Type,
context: JsonDeserializationContext
): Array<*>? {
- val componentType: Type = when (typeOfT) {
- is GenericArrayType -> typeOfT.genericComponentType
- is Class<*> -> if (typeOfT.isArray) {
- typeOfT.componentType
- } else {
- throw JsonParseException("Expected array: $typeOfT")
- }
+ val componentType: Type = (typeOfT as? GenericArrayType)?.genericComponentType
+ ?: throw JsonParseException("Expected a GenericArrayType, but got: $typeOfT")
+
+ val items: List<Any?> = jsonToList(json, componentType, context)
+
+ return items.listToArray(componentType)
+ }
+
+ private fun List<Any?>.listToArray(componentType: Type): Array<Any?> {
+ val arr = JArray.newInstance(getRawType(componentType), size)
+ forEachIndexed { i, v -> JArray.set(arr, i, v) }
+ @Suppress("UNCHECKED_CAST")
+ return arr as Array<Any?>
+ }
+}
- else -> throw JsonParseException("Unsupported type: $typeOfT")
- }
- val raw: Class<*> = when (componentType) {
- is Class<*> -> componentType
- is ParameterizedType -> componentType.rawType as Class<*>
- else -> Any::class.java
+/**
+ * Deserializes a JSON value that can be either an array or an object with numeric string keys.
+ * Examples:
+ * - Array: ["item1", "item2"]
+ * - Object with numeric keys: {"0": "item1", "1": "item2"}
+ * - Non-ordered numeric keys example: {"2":"a","1":"b","10":"z"} -> ["b", "a", "z"]
+ *
+ * This deserializer handles both cases and converts them into a List<T>.
+ * - For arrays: elements are deserialized in their natural order.
+ * - For objects: keys must be numeric strings (0, 1, 2, ...). Values are read by sorting keys numerically
+ * (0, 1, 2, ...), not by insertion order. Any non-numeric key results in a JsonParseException.
+ * If the JSON value is null, null is returned.
+ */
+class ListOrObjectDeserializer : JsonDeserializer<List<*>> {
+ override fun deserialize(
+ json: JsonElement,
+ typeOfT: Type,
+ context: JsonDeserializationContext
+ ): List<*>? {
+ val itemType = (typeOfT as ParameterizedType).actualTypeArguments[0]
+ return jsonToList(json, itemType, context)
+ }
+}
+
+private fun jsonToList(
+ json: JsonElement,
+ itemType: Type,
+ context: JsonDeserializationContext
+): List<Any?> {
+ return when {
+ json.isJsonArray -> json.asJsonArray.map { element ->
+ context.deserialize(element, itemType)
}
- val items: List<Any?> = when {
- json.isJsonArray -> json.asJsonArray.map { context.deserialize(it, componentType) }
- json.isJsonObject -> json.asJsonObject.entrySet()
- .onEach {
- it.key.toIntOrNull() ?: throw JsonParseException("Unexpected key: ${it.key}")
- }
- .sortedBy { it.key.toInt() }
- .map { context.deserialize(it.value, componentType) }
-
- json.isJsonNull -> return null
- else -> throw JsonParseException("Unexpected JSON for Array: $json")
- }
- val arr = java.lang.reflect.Array.newInstance(raw, items.size)
- items.forEachIndexed { i, v -> java.lang.reflect.Array.set(arr, i, v) }
- @Suppress("UNCHECKED_CAST")
- return arr as Array<*>
+
+ json.isJsonObject -> json.asJsonObject.entrySet()
+ .onEach { entry ->
+ val key = entry.key
+ key.toIntOrNull()
+ ?: throw JsonParseException("Unexpected key in JSON object matching array: $key")
+ }
+ .sortedBy { it.key.toInt() }
+ .map { entry ->
+ context.deserialize(entry.value, itemType)
+ }
+
+ json.isJsonNull -> emptyList()
+ else -> throw JsonParseException("Unexpected JSON type for List, $json")
}
}
Index: libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ListOrObjectDeserializer.kt
===================================================================
diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ListOrObjectDeserializer.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ListOrObjectDeserializer.kt
deleted file mode 100644
--- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/ListOrObjectDeserializer.kt (revision 88217ddda7dd0897c76a173d49272a1e6133797e)
+++ /dev/null (revision 88217ddda7dd0897c76a173d49272a1e6133797e)
@@ -1,50 +0,0 @@
-package org.wordpress.android.fluxc.network.rest
-
-import com.google.gson.JsonDeserializationContext
-import com.google.gson.JsonDeserializer
-import com.google.gson.JsonElement
-import com.google.gson.JsonParseException
-import java.lang.reflect.ParameterizedType
-import java.lang.reflect.Type
-
-/**
- * Deserializes a JSON value that can be either an array or an object with numeric string keys.
- * Examples:
- * - Array: ["item1", "item2"]
- * - Object with numeric keys: {"0": "item1", "1": "item2"}
- * - Non-ordered numeric keys example: {"2":"a","1":"b","10":"z"} -> ["b", "a", "z"]
- *
- * This deserializer handles both cases and converts them into a List<T>.
- * - For arrays: elements are deserialized in their natural order.
- * - For objects: keys must be numeric strings (0, 1, 2, ...). Values are read by sorting keys numerically
- * (0, 1, 2, ...), not by insertion order. Any non-numeric key results in a JsonParseException.
- * If the JSON value is null, null is returned.
- */
-class ListOrObjectDeserializer : JsonDeserializer<List<*>> {
- override fun deserialize(
- json: JsonElement,
- typeOfT: Type,
- context: JsonDeserializationContext
- ): List<*>? {
- val itemType = (typeOfT as ParameterizedType).actualTypeArguments[0]
- return when {
- json.isJsonArray -> json.asJsonArray.map { element ->
- context.deserialize<Any?>(element, itemType)
- }
-
- json.isJsonObject -> json.asJsonObject.entrySet()
- .onEach { entry ->
- val key = entry.key
- key.toIntOrNull()
- ?: throw JsonParseException("Unexpected key in JSON object matching array: $key")
- }
- .sortedBy { it.key.toInt() }
- .map { entry ->
- context.deserialize<Any?>(entry.value, itemType)
- }
-
- json.isJsonNull -> null
- else -> throw JsonParseException("Unexpected JSON type for List, $json")
- }
- }
-}| val componentType: Type = when (typeOfT) { | ||
| is GenericArrayType -> typeOfT.genericComponentType | ||
| is Class<*> -> if (typeOfT.isArray) { | ||
| typeOfT.componentType | ||
| } else { | ||
| throw JsonParseException("Expected array: $typeOfT") | ||
| } | ||
|
|
||
| else -> throw JsonParseException("Unsupported type: $typeOfT") | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np, as this will be called only for arrays, we can be sure that the type here is GenericArrayType, so we can simplify this to something like this:
val componentType: Type = (typeOfT as? GenericArrayType)?.genericComponentType
?: throw JsonParseException("Expected a GenericArrayType, but got: $typeOfT")There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Your patch makes it simpler and better.
| val raw: Class<*> = when (componentType) { | ||
| is Class<*> -> componentType | ||
| is ParameterizedType -> componentType.rawType as Class<*> | ||
| else -> Any::class.java | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np, we can use the GsonTypes.getRawType function to get the raw type directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good advice!
This commit refactors the `ArrayOrObjectDeserializer` and `ListOrObjectDeserializer` classes. Additionally, `ListOrObjectDeserializer.kt` has been removed, and its contents were moved into `ArrayOrObjectDeserializer.kt` since they are closely related.
|
Thank you for the patch, @hichamboushaba! It includes useful improvements, and I applied it directly. 🙇🏻♂️ |
Closes WOOMOB-1037
Description
This adds support for parsing
"meta_data": {"1": {...}, "2": {...}}when the expected format is"meta_data": […]. This unexpected format can sometimes be sent by the server due to PHP behavior.Previously, we added this for the
List<*>type, but we're still seeing crashes, so we're now adding it forArray<*>type as well. This was discussed in #14553 (comment)Steps to reproduce
I don’t know how to reproduce this case. Reviewing the unit tests should be enough.
The tests that have been performed
Images/gif
RELEASE-NOTES.txtif necessary. Use the "[Internal]" label for non-user-facing changes.