-
Gson does not specify deserialize
with @NotNull
parameters, it is possible to get an NPE, and not know why (without a lot of digging).
-
You define (and construct) PercentResponse
but return PercentInspectionResponse
, same with TypePercentInspection
.
-
You don’t use @SerializedName
. While Gson allows this, it couples your implementation to the outside world in an unsatisfactory way. You want to be able to change the two independently.
-
You are checking an intput (type
) that has disjoint (non-overlapping) values (integers) with two if
s. This is not a “bug”, but a code smell.
-
You probably did not try to compile this code. As you define data classes but try to use them like normal classes.
-
Creating a Gson object inside your deserializer.
Here is a quick conversion. It’s based on my limited understanding of the requirements.
// All code compiled with Kotlin 1.3.10
class ResponseDeserializer : JsonDeserializer<Response> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Response {
checkNotNull(json) // throws error here, instead of somewhere inside Gson.
checkNotNull(typeOfT)
checkNotNull(context)
val jsonObject = json.asJsonObject
val type = jsonObject.get("type").asInt
return Response(
jsonObject.get("orderId").asInt,
type,
jsonObject.get("note").asStringOrNull,
when (type) {
1 -> jsonObject.extractList<PercentItem.PercentType1>("percent", context)
2 -> jsonObject.extractList<PercentItem.PercentType2>("percent", context)
else -> TODO("Unexpected percent type")
}
)
}
private inline fun <reified T> JsonObject.extractList(memberName: String, context: JsonDeserializationContext): List<T> {
return this.getAsJsonArray(memberName).map {
context.deserialize<T>(it, T::class.java)
}
}
}
private val JsonElement.asStringOrNull: String?
get() = when {
this.isJsonNull -> null
else -> this.asString
}
Models
data class Response(
@SerializedName("orderId")
val orderId: Int,
@SerializedName("type")
val type: Int,
@SerializedName("note")
val note: String? = null,
@SerializedName("percent")
val percent: List<PercentItem> = emptyList()
)
sealed class PercentItem {
data class PercentType1(
@SerializedName("category")
val category: String,
@SerializedName("items")
val items: List<Item>?
) : PercentItem()
data class PercentType2(
@SerializedName("id")
val id: String,
@SerializedName("itembudgetID")
val itemBudgetID: Int,
@SerializedName("powID")
val powID: Int,
@SerializedName("itemno")
val itemNo: String,
@SerializedName("info_description")
val infoDescription: String,
@SerializedName("description")
val description: String,
@SerializedName("subdescription")
val subDescription: String,
@SerializedName("linepercent")
val linePercent: Int
) : PercentItem()
}
data class Item(
@SerializedName("categoryID")
val categoryID: Int,
@SerializedName("category_check")
val categoryCheck: Boolean,
@SerializedName("itembudgetID")
val budgetID: Int,
@SerializedName("description")
val description: String,
@SerializedName("cccategoryname")
val categoryName: String
)
Tests, mostly just seeing if the deserializer can make sense of the input.
class ResponseDeserializerTest {
@Test
fun type1Response_isNotNull() {
val response = extract(inputType1)
assertNotNull(response)
}
@Test
fun type2Response_isNotNull() {
val response = extract(inputType2)
assertNotNull(response)
}
@Test
fun type1Response_hasPercentType1Items() {
val response = extract(inputType1)
val list = response.percent.filterIsInstance(PercentItem.PercentType1::class.java)
assertTrue(list.isNotEmpty())
}
@Test
fun type1Response_hasNoPercentType2Items() {
val response = extract(inputType1)
val list = response.percent.filterIsInstance(PercentItem.PercentType2::class.java)
assertTrue(list.isEmpty())
}
@Test
fun type1Response_hasOnlyPercentType1Items() {
val response = extract(inputType1)
val all = response.percent.all {
when (it) {
is PercentItem.PercentType1 -> true
else -> false
}
}
assertTrue(all)
}
@Test
fun type1Response_hasOnlyPercentType1ItemsFails() {
val response = extract(inputType1Broken)
val all = response.percent.all {
when (it) {
is PercentItem.PercentType1 -> !it.category.isNullOrBlank()
else -> false
}
}
assertFalse(all)
}
@Test
fun type2Response_hasPercentType2Items() {
val response = extract(inputType2)
val list = response.percent.filterIsInstance(PercentItem.PercentType2::class.java)
assertTrue(list.isNotEmpty())
}
@Test
fun type2Response_hasNoPercentType1Items() {
val response = extract(inputType2)
val list = response.percent.filterIsInstance(PercentItem.PercentType1::class.java)
assertTrue(list.isEmpty())
}
@Test
fun responseList_isNotNull() {
val response = extractArray(inputTypeArray)
assertNotNull(response)
}
private val gson: Gson = GsonBuilder()
.registerTypeAdapter(Response::class.java, ResponseDeserializer())
.create()
private fun extract(input: String): Response {
return gson.fromJson(input, Response::class.java)
}
private fun extractArray(input: String): List<Response> {
val type = object : TypeToken<List<Response>>() {}.type
return gson.fromJson(input, type)
}
private val inputType1 = """{
"orderId": 123121,
"note": "notas raul",
"percent": [
{
"category": "Items",
"items": [
{
"category_check": false,
"cccategoryID": 10,
"cccategoryname": " Items",
"itembudgetID": 122,
"description": "Sewer / Septic"
}
]
}
],
"type": 1
}"""
private val inputType2 = """{
"orderId": 123123,
"note": null,
"percent": [
{
"id": "123123_1",
"itembudgetID": 123123,
"powID": 0,
"itemno": "1",
"info_description": "blue",
"description": "description",
"subdescription": "",
"linepercent": 0
}
],
"type": 2
}"""
private val inputType1Broken = """{
"orderId": 1234,
"note": "Nada",
"percent": [
{
"id": "123123_1",
"itembudgetID": 123123,
"powID": 0,
"itemno": "1",
"info_description": "blue",
"description": "description",
"subdescription": "",
"linepercent": 0
}, {
"category": "Items",
"items": [
{
"category_check": false,
"cccategoryID": 10,
"cccategoryname": " Items",
"itembudgetID": 122,
"description": "Sewer / Septic"
}
]
}
],
"type": 1
}"""
private val inputTypeArray = """[$inputType1, $inputType2]"""
}
This code does not handle consistency checking. If you hand it a JSON string with "type": 1
, but where the internal percent
array does not match it will give you objects with nulls. Gson does not respect Kotlin’s property conventions.