Json field null 에서 empty 값 변경

서버와 데이터를 주고 받을 때 Json 형식으로 주고 받는데 서버에서 내려주는 데이터 field 가 가끔씩 의도하지 않은 null 값이 내려올 때가 있다.
Kotlin 언어를 사용할 때 nullable 타입인지 아닌지 구분해서 필드를 만들 수 있는데 이 경우는 예상하지 못하기 때문에 무조건 nullable 형태로 사용해야 한다.
이렇게 사용하면 뭔가 깔끔하지 못한 느낌이 들어서 다른 해결 방안을 생각해 보았다.

이 문제를 해결한 방법은 gson 생성 시 TypeAdapter 를 커스텀해서 사용하는 방법이었다.

우선 array 배열에 [null] 형식으로 오는 값과 필드(Boolean , Long , String)가 null 로 오는 두 케이스를 다뤘다.

class GsonHelperTest {
    @Test
    fun `Null to Empty Value`() {
        val jsonString = "{\n" +
                "\"stringValue\": null,\n" +
                "\"timestamp\": \"1602158882721\",\n" +
                "\"longValue\": null,\n" +
                "\"intValue\": null,\n" +
                "\"booleanValue\": null,\n" +
                "\"items\": [null]\n" +
                "    }"

        val gson = GsonBuilder()
                .registerTypeAdapter(ArrayList::class.java, NonNullListDeserializer<Any>())
                .registerTypeAdapter(String::class.java, StringTypeAdapter())
                .registerTypeAdapter(Long::class.java, LongTypeAdapter())
                .registerTypeAdapter(Boolean::class.java, BooleanTypeAdapter())
                .disableHtmlEscaping()
                .create()

        val nonNullData: NonNullData = gson.fromJson(jsonString, NonNullData::class.java)

        println(nonNullData.stringValue.isEmpty())
        println(nonNullData.timestamp)
        println(nonNullData.items.size)
        println(nonNullData.longValue)
        println(nonNullData.intValue)
        println(nonNullData.booleanValue)

        assert(
                nonNullData.stringValue.isEmpty()
        )
    }

    data class NonNullData(
            @SerializedName("stringValue")
            var stringValue: String = "",
            @SerializedName("timestamp")
            var timestamp: String = "",
            @SerializedName("items")
            var items: ArrayList<Long> = arrayListOf(),
            @SerializedName("longValue")
            var longValue: Long = -1L,
            @SerializedName("intValue")
            var intValue: Int = -1,
            @SerializedName("booleanValue")
            var booleanValue: Boolean = true
    )
}

class NonNullListDeserializer<T> : JsonDeserializer<ArrayList<T>> {
    @Throws(JsonParseException::class)
    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ArrayList<T> {
        if (json is JsonArray) {
            val array = json
            val size = array.size()
            if (size == 0) {
                return arrayListOf()
            }
            val list: ArrayList<T> = ArrayList(size)
            for (i in 0 until size) {
                val elementType: Type = `$Gson$Types`.getCollectionElementType(typeOfT, ArrayList::class.java)
                val value: T = context.deserialize(array[i], elementType)
                if (value != null) {
                    list.add(value)
                }
            }
            return list
        }
        return arrayListOf()
    }
}


class StringTypeAdapter : TypeAdapter<String>() {
    override fun write(out: JsonWriter, value: String?) {
    }

    override fun read(`in`: JsonReader): String {
        if (`in`.peek() === JsonToken.NULL) {
            `in`.nextNull()
            return ""
        }
        return `in`.nextString()
    }

}

Booelan, Long Type Adapter  같은 구조라 생략

NonNullListDeserializer 를 사용해서 items: [null] 식으로 오는 데이터를 items [] 로 변환 할 수 있고 StringTypeAdapter 를 사용해서 String 으로 보내주는 경우인데 null 값으로 보내주는 경우 null 값이 아닌 “” 빈값 으로 변경 시켜 줄 수 있다.

아래는 테스트 결과 이다.

Retrofit 에 적용 시키는 경우에는 아래와 같이 적용하면 된다.

    private fun createRetrofitClient(okHttpClient: OkHttpClient): Retrofit {
        val builder = Retrofit.Builder()
                .baseUrl(baseUrl)
                .addCallAdapterFactory(rxJava2CallAdapterFactory)
                .addConverterFactory(GsonConverterFactory.create(
                        GsonBuilder().registerTypeAdapter(ArrayList::class.java, NonNullListDeserializer<Any>())
                                .registerTypeAdapter(String::class.java, StringTypeAdapter())
                                .registerTypeAdapter(Long::class.java, LongTypeAdapter())
                                .registerTypeAdapter(Boolean::class.java, BooleanTypeAdapter())
                                .disableHtmlEscaping()
                                .create())
                )
                .addConverterFactory(enumConverterFactory)
        return builder.client(okHttpClient).build()
    }

위와 같이 사용하게 되면 데이터 모델을 만들때 NonNull 타입으로 만들어도 해당 필드나 List 가 null 이 되지 않는걸 신뢰할 수 있다.