RedSIght
RedSIght

Reputation: 728

Moshi fail to deserialize 0, 1 to "Boolean?"

We try to deserialize JSON field from 0, 1, null to "Boolean?" And we have this customize Annotation:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class BooleanType

class BooleanAdapter {
    @FromJson
    @BooleanType
    fun fromJson(value: Int): Boolean {
        return value == 1
    }

    @ToJson
    fun toJson(@BooleanType value: Boolean): Int {
        return if (value) 1 else 0
    }
}

Everything works fine if the field is 0 or 1. But when it comes to "null", it always throw exception

No JsonAdapter for class java.lang.Boolean annotated [@com.pk.data.serialize.BooleanType()]
....

Here is the dummy data and unit test

@JsonClass(generateAdapter = true)
data class HaHaData(
    @BooleanType
    @Json(name = "haha") val haha: Boolean?,
)


 @Test
    fun parseNullBooleanTest() {
        val moshi = Moshi.Builder()
            .add(BooleanAdapter())
            .build()
        val json = "{\"haha\":1}"
        val jsonWithNull = "{\"haha\":null}"
        val data = HaHaDataJsonAdapter(moshi).fromJson(json)
        val dataWithNull = HaHaDataJsonAdapter(moshi).fromJson(jsonWithNull) // exception thrown
        assert(data?.haha== true)
        assert(dataWithNull?.haha == null)
    }

Upvotes: 1

Views: 838

Answers (2)

AndroidKotlinNoob
AndroidKotlinNoob

Reputation: 548

The solution to the problem has already been provided by @AlexT. The input types in the adapter have to be nullable and if the output type is nullable (as it is in the original question), it also has to be nullable in the Adapter.

So a working adapter could look like this:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class BooleanType

class BooleanAdapter {
    @FromJson
    @BooleanType
    fun fromJson(value: Int?): Boolean? {
        return when (value) {
            null -> null
            1 -> true
            else -> false
        }
    }

    @ToJson
    fun toJson(@BooleanType value: Boolean?): Int? {
        return when (value) {
            null -> null
            true -> 1
            false -> 0
        }
    }
}

But for all those people who need an Adapter that can accept both an Int and a Boolean (and a String) at the same time, here's the solution for that:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class BooleanType

class BooleanAdapter {
    
    @FromJson
    @BooleanType
    fun fromJson(reader: JsonReader): Boolean? {
        return when (reader.peek()) {
            JsonReader.Token.NULL -> reader.nextNull()
            JsonReader.Token.BOOLEAN -> reader.nextBoolean()
            JsonReader.Token.NUMBER -> when (val value = reader.nextInt() {
                0 -> false
                1 -> true
                else -> throw JsonDataException("Number $value can not be transformed to a Boolean.")
            }
            JsonReader.Token.STRING -> when (val value = reader.nextString()) {
                "false".equals(value, ignoreCase = true) -> false
                "true".equals(value, ignoreCase = true) -> true
                "0" == value -> false
                "1" == value -> true
                else -> throw JsonDataException("String $value can not be transformed to a Boolean.")
            }
            else -> throw JsonDataException("Unknown value can not be transformed to a Boolean")
        }
    }
    
    @ToJson
    fun toJson(@BooleanType value: Boolean?): Boolean? {
        // This adapter is not intended to be used for serializing, as it is impossible for it to know what the output type should be.
        return value
    }
}

or if you prefer to use Any? as an input type instead of JsonReader:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class BooleanType

class BooleanAdapter {
    
    @FromJson
    @BooleanType
    fun fromJson(value: Any?): Boolean? {
        return when (value) {
            null -> null
            is Boolean -> value
            is Number -> when (value.toInt()) {
                0 -> false
                1 -> true
                else -> throw JsonDataException("Number $value can not be transformed to a Boolean.")
            }
            is String -> when {
                "true".equals(value, ignoreCase = true) -> true
                "false".equals(value, ignoreCase = true) -> false
                "1" == value -> true
                "0" == value -> false
                else -> throw JsonDataException("String $value can not be transformed to a Boolean.")
            }
            else -> throw JsonDataException("Unknown value $value can not be transformed to a Boolean.")
        }
    }
    
    @ToJson
    fun toJson(@BooleanType value: Boolean?): Boolean? {
        // This adapter is not intended to be used for serializing, as it is impossible for it to know what the correct output type should be.
        return value
    }
}

Upvotes: 1

AlexT
AlexT

Reputation: 2984

Your fromJson does not accept null as an input.

If you want null to become false:

fun fromJson(value: Int?): Boolean {
      return value == 1
}

If you want null to stay null (you'll also have to modify the toJson fun to accept nulls as well then):

fun fromJson(value: Int?): Boolean? {
    return value?.let { it == 1 }
}

Upvotes: 2

Related Questions