Mapping sealed class using Gson

549 views Asked by At

I am getting the following error whilst trying to map an incoming REST-api result in Json to an object :

failed to invoke private kotlinx.serialization.json.jsonelement() with no args

After some searching the problem seems to be that I am trying to instantiate a Value() object whilst it's a sealed class. Now I'm not sure how to solve this ?

data class Device (
    @SerializedName(value="DeviceSettings")
    val deviceSettings: Map<String, Value>? = null,
    @SerializedName(value="Id")
    val id: String
)

sealed class Value {
    class BoolValue(val value: Boolean)  : Value()
    class DoubleValue(val value: Double) : Value()
    class IntegerValue(val value: Long)  : Value()
    class StringValue(val value: String) : Value()
}

The problem occurs when the deviceSettings mapping is being constructed. Because it might contain several types of data the Value sealed class is defined. But as mentioned this will simply crash whilst beings parsed...

The idea is that the DeviceSettings Map is dynamic, that's why it's not actually parsed into a predefined data class for example ! It might contain a single key:value pair but there might also be thirty !

@cutiko Thats correct, Hereby some of the received json :

"DeviceSettings": {
    "ToneLevel": 80.0,
    "AutoLogging": false,
    "OrientationLandscape": false,
    "AudioDuration": 2500.0,
    "ShoutLevel": 1.0,
    "SipExtension": 0.0,
    "SipServerAuthenticationKey": "",
    "SipServerPort": 5060.0,
    "SipServerUri": ""
}
1

There are 1 answers

0
Marcono1234 On

You will have to write a custom TypeAdapter which peeks at the type of the JSON data and depending on it chooses the correct Value subclass to instantiate. For example:

object ValueTypeAdapter : TypeAdapter<Value>() {
    override fun write(writer: JsonWriter, value: Value?) {
        throw UnsupportedOperationException()
    }

    override fun read(reader: JsonReader): Value {
        return when (val token = reader.peek()) {
            JsonToken.BOOLEAN -> Value.BoolValue(reader.nextBoolean())
            JsonToken.STRING -> Value.StringValue(reader.nextString())
            JsonToken.NUMBER -> {
                // Read the string representation of the number
                val numberAsStr = reader.nextString()
                // If number contains decimal point or exponent prefix 'e', parse as Double
                if (numberAsStr.any { c -> c == '.' || c == 'e' || c == 'E' }) {
                    Value.DoubleValue(numberAsStr.toDouble())
                } else {
                    Value.IntegerValue(numberAsStr.toLong())
                }
            }
            else -> throw IllegalArgumentException("Unexpected value type $token at ${reader.path}")
        }
    }
}

You would then register the adapter on a GsonBuilder like this:

val gson = GsonBuilder()
    .registerTypeAdapter(Value::class.java, ValueTypeAdapter)
    .create()

There are however a few things to consider, so you might have to adjust this code:

  • How do you want to treat -0? With the code above it would be parsed as Long and the - sign would be lost. Maybe you want to parse it as Double -0.0 instead.
  • In your example JSON data all numbers have a decimal point; should those all be parsed as Double or do you have a different logic for deciding what is a Double or a Long?
  • The original exception message you showed seems unrelated to the problem you mentioned, at least the connection to kotlinx.serialization is not clear.