Custom KotlinX Serializer for List class - json

I want to make a custom List serializer that will parse invalid json arrays safely. Example: list of Int [1, "invalid_int", 2] should be parsed as [1, 2].
I've made a serializer and added it to Json provider, but serialization keeps failing after first element and cannot continue, so I'm getting list of 1 element [1], how can I handle invalid element correctly so decoder will keep parsing other elements?
class SafeListSerializerStack<E>(val elementSerializer: KSerializer<E>) : KSerializer<List<E>> {
override val descriptor: SerialDescriptor = ListSerializer(elementSerializer).descriptor
override fun serialize(encoder: Encoder, value: List<E>) {
val size = value.size
val composite = encoder.beginCollection(descriptor, size)
val iterator = value.iterator()
for (index in 0 until size) {
composite.encodeSerializableElement(descriptor, index, elementSerializer, iterator.next())
}
composite.endStructure(descriptor)
}
override fun deserialize(decoder: Decoder): List<E> {
val arrayList = arrayListOf<E>()
try {
val startIndex = arrayList.size
val messageBuilder = StringBuilder()
val compositeDecoder = decoder.beginStructure(descriptor)
while (true) {
val index = compositeDecoder.decodeElementIndex(descriptor) // fails here on number 2
if (index == CompositeDecoder.DECODE_DONE) {
break
}
try {
arrayList.add(index, compositeDecoder.decodeSerializableElement(descriptor, startIndex + index, elementSerializer))
} catch (exception: Exception) {
exception.printStackTrace() // falls here when "invalid_int" is parsed, it's ok
}
}
compositeDecoder.endStructure(descriptor)
if (messageBuilder.isNotBlank()) {
println(messageBuilder.toString())
}
} catch (exception: Exception) {
exception.printStackTrace() // falls here on number 2
}
return arrayList
}
}
Error happens after invalid element is parsed and exception is thrown at compositeDecoder.decodeElementIndex(descriptor) line with:
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 4: Expected end of the array or comma
JSON input: [1, "invalid_int", 2]
I had a feeling that it should "swallow" invalid element and just keep moving, but instead it's stuck and cannot continue parsing, which doesn't make sense to me.

This could be done without custom serializer. Just parse everything as a String (specify isLenient = true to allow unquoted strings) and then convert to Int all valid integers:
fun main() {
val input = "[1, \"invalid_int\", 2]"
val result: List<Int> = Json { isLenient = true }
.decodeFromString<List<String>>(input)
.mapNotNull { it.toIntOrNull() }
println(result) // [1, 2]
}
In a more generic case (when the list is a field and/or its elements are not simple Ints), you'll need a custom serializer:
class SafeListSerializerStack<E>(private val elementSerializer: KSerializer<E>) : KSerializer<List<E>> {
private val listSerializer = ListSerializer(elementSerializer)
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun serialize(encoder: Encoder, value: List<E>) {
listSerializer.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): List<E> = with(decoder as JsonDecoder) {
decodeJsonElement().jsonArray.mapNotNull {
try {
json.decodeFromJsonElement(elementSerializer, it)
} catch (e: SerializationException) {
e.printStackTrace()
null
}
}
}
}
Note that this solution works only with deserialization from the Json format and requires kotlinx.serialization 1.2.0+

Found a way, we can extract json array from decoder given we are using Json to parse it
override fun deserialize(decoder: Decoder): List<E> {
val jsonInput = decoder as? JsonDecoder
?: error("Can be deserialized only by JSON")
val rawJson = jsonInput.decodeJsonElement()
if (rawJson !is JsonArray) {
return arrayListOf()
}
val jsonArray = rawJson.jsonArray
val jsonParser = jsonInput.json
val arrayList = ArrayList<E>(jsonArray.size)
jsonArray.forEach { jsonElement ->
val result = readElement(jsonParser, jsonElement)
when {
result.isSuccess -> arrayList.add(result.getOrThrow())
result.isFailure -> Log.d("ERROR", "error parsing array")
}
}
arrayList.trimToSize()
return arrayList
}
private fun readElement(json: Json, jsonElement: JsonElement): Result<E> {
return try {
Result.success(json.decodeFromJsonElement(elementSerializer, jsonElement))
} catch (exception: Exception) {
Result.failure(exception)
}
}

Related

kotlin: nested polymorphic serialization with generics

I want to serialize Map<String, Any> and one of the values type is Pair<Int, Int>.
How to register the Pair as polymorphic subclass for that?
val module = SerializersModule {
polymorphic(Any::class) {
subclass(Int::class, PolymorphicPrimitiveSerializer(Int.serializer()))
subclass(String::class, PolymorphicPrimitiveSerializer(String.serializer()))
subclass(Pair::class, PolymorphicSerializer(Pair::class))
}
}
val format = Json { serializersModule = module }
val mm = mapOf<String, Any>()
.plus("int-int pair") to (5 to 10))
val jsoned = format.encodeToString(mm)
val mmDecoded = format.decodeFromString(jsoned)
require(mm==mmDecoded)
should encode to json like:
[{"first": "int-int pair",
"second":{"type": "Pair", "value":
{"first": {"type": Int, "value":5}, "second": {"type":Int, "value": 10}}}}]
But produce the following error:
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalArgumentException: Serializer for Pair
can't be registered as a subclass for polymorphic serialization
because its kind OPEN is not concrete. To work with multiple
hierarchies, register it as a base class. at
kotlinx.serialization.json.internal.PolymorphismValidator.checkKind(PolymorphismValidator.kt:41)
at
kotlinx.serialization.json.internal.PolymorphismValidator.polymorphic(PolymorphismValidator.kt:31)
at
kotlinx.serialization.modules.SerialModuleImpl.dumpTo(SerializersModule.kt:189)
at
kotlinx.serialization.json.JsonImpl.validateConfiguration(Json.kt:358)
at kotlinx.serialization.json.JsonImpl.(Json.kt:352) at
kotlinx.serialization.json.JsonKt.Json(Json.kt:189) at
kotlinx.serialization.json.JsonKt.Json$default(Json.kt:185) at
MainKt.(Main.kt:143)
I could not make it work with polymorphic subclasses, but I don't think that feature is intended for use with Any and primitives anyway (see this question). A custom serializer seems like a more appropriate and simpler solution and unlike the polymorphic serialization, it doesn't require too much custom serializer code:
#ExperimentalSerializationApi
class DynamicLookupSerializer: KSerializer<Any> {
override val descriptor: SerialDescriptor = ContextualSerializer(Any::class, null, emptyArray()).descriptor
#OptIn(InternalSerializationApi::class)
override fun serialize(encoder: Encoder, value: Any) {
val actualSerializer = encoder.serializersModule.getContextual(value::class) ?: value::class.serializer()
encoder.encodeSerializableValue(actualSerializer as KSerializer<Any>, value)
}
override fun deserialize(decoder: Decoder): Any {
return try {
PairSerializer(Int.serializer(), Int.serializer()).deserialize(decoder)
} catch (e: Throwable) {
try {
decoder.decodeInt()
} catch (e: Throwable) {
decoder.decodeString()
}
}
}
}
val module = SerializersModule {
contextual(Any::class, DynamicLookupSerializer())
contextual(Pair::class) {
PairSerializer(Int.serializer(), Int.serializer())
}
}
val format = Json { serializersModule = module }
val mm = mapOf<String, Any>()
.plus("int-int pair" to (5 to 10))
.plus("int" to 6)
.plus("string" to "some string")
.plus("another-int" to 86248726)
.plus("another-pair" to (56 to 961))
val jsoned = format.encodeToString(mm)
println(jsoned)
val mmDecoded = format.decodeFromString<Map<String, Any>>(jsoned)
require(mm==mmDecoded)
In this custom serializer, we find the actual serializer for value: Any when serializing by looking it up via its class (value::class). As a result, PairSerializer(Int.serializer(), Int.serializer()) has to be registered too so that it will be found in DynamicLookupSerializer.serialize. When deserializing, we try the supported serializers one by one (string, int, and pair of ints).
I do realize that this is not the nicest solution due to the try-catches but it does work and it's simple enough.
I solved it by providing custom Polymorphic serializer similar to how I serialized the map:
https://github.com/assafshouval/PolymorphicMapSerializer/blob/master/src/main/kotlin/Main.kt
import kotlinx.serialization.builtins.*
val pairAnyAnySerializer = PairSerializer(
PolymorphicSerializer(Any::class), PolymorphicSerializer(Any::class)
) as <KSerializer<Pair<*,*>>>()
and for every type that is defined in the serializers module for polymorphic serializatin of Any it will serialize/deserialze correct.
val json = Json {
serializersModule = SerializersModule {
polymorphic(Any::class) {
subclass(String::class, PolymorphicPrimitiveSerializer(String.serializer()))
subclass(Int::class, PolymorphicPrimitiveSerializer(Int.serializer()))
}
}
}

Nested JSON Objects in Kotlin with Volley

I am very new to this as you can probably tell, but i'm trying to parse a JSON url with Volley using Kotlin in Android Studio. The url contains nested Objects, not nested Arrays.
I can display everything inside "questionnaire", but I only want to display "typeOfQuestion". How do i do that?
MainActivity.kt:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
questionTV = findViewById(R.id.idTVQuestion)
answerTV = findViewById(R.id.idTVAnswer)
typeTV = findViewById(R.id.idTVType)
val queue: RequestQueue = Volley.newRequestQueue(applicationContext)
val request = JsonObjectRequest(Request.Method.GET, url, null, { response ->
loadingPB.setVisibility(View.GONE)
try {
val question: String = response.getString("question")
val answer: String = response.getString("answer")
val typeOfQuestion: String = response.getString("typeOfQuestion")
questionTV.text = question
answerTV.text = answer
typeTV.text = typeOfQuestion
} catch (e: Exception) {
e.printStackTrace()
}
}, { error ->
Log.e("TAG", "RESPONSE IS $error")
Toast.makeText(this#MainActivity, "Fail to get response", Toast.LENGTH_SHORT)
.show()
})
queue.add(request)
}
}
Heres the JSON:
{
"questionnaire": {
"question": "Where do you live?",
"answer": "In the mountains",
"typeOfQuestion": "Informative
}
}
You have object inside another json object.If you need to access field from child object you need to get child jsonObject and then get fields from object.
var questionnaire = response.getJSONObject("questionnaire")
You need to get fields from questionnaire object.Like.
val question: String = questionnaire.getString("question")
val answer: String = questionnaire.getString("answer")
val typeOfQuestion: String = questionnaire.getString("typeOfQuestion")

Skipping serialization of null values depending on the type

I started working with moshi a couple of weeks ago, so maybe I am missing something trivial, but I spent already quite a bit of time trying to fix this without success, so here is my question.
Having the following reproducible code:
fun main() {
val moshi = Moshi.Builder().add(OptionalAdapter).build()
val objectToSerialize = DummyObject()
val json = moshi.adapter(DummyObject::class.java).serializeNulls().toJson(objectToSerialize)
println(json)
}
#JsonClass(generateAdapter = true)
data class DummyObject(val value: Int=123, val someNullable: String? = null,
val someNotPresent: Optional<String> = Optional.NotPresent,
val somePresent: Optional<String> = Optional.Present("aaaa"))
class OptionalAdapter<T>(private val valueAdapter: JsonAdapter<T>) : JsonAdapter<Optional<T>>() {
#Suppress("UNCHECKED_CAST")
override fun fromJson(reader: JsonReader) = Optional.Present(valueAdapter.fromJson(reader) as T)
override fun toJson(writer: JsonWriter, value: Optional<T>?) {
when (value) {
is Optional.NotPresent -> writer.nullValue()
is Optional.Present -> valueAdapter.serializeNulls().toJson(writer, value.value)
}
}
companion object Factory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
return if (Types.getRawType(type) == Optional::class.java && annotations.isEmpty()) {
val valueType = if(type is ParameterizedType) {
type.actualTypeArguments.get(0)
} else {
//Should not happen
throw IllegalArgumentException()
}
return OptionalAdapter(moshi.adapter<Any>(valueType).nullSafe())
} else {
null
}
}
}
}
sealed class Optional<out T> {
val provided get() = this !is NotPresent
abstract val value: T
object NotPresent : Optional<Nothing>() {
// have the IDE raise an error if the user knows a type is missing but still tries to access a value
#Deprecated(
"Cannot access a missing value",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("TODO(\"value is missing\")")
)
override val value: Nothing
get() = error("cannot access provided field")
}
data class Present<out T>(override val value: T) : Optional<T>()
}
I would like to serialize as {"value":123,"someNullable":null,"somePresent":"aaaa"} instead of {"value":123,"someNullable":null,"someNotPresent":null,"somePresent":"aaaa"}, which is what is doing now.
Basically, I want to skip the serialization in case the type is Optional.NotPresent. Any suggestion?
The solution I ended up with:
override fun toJson(writer: JsonWriter, value: Optional<T>?) {
when (value) {
is Optional.NotPresent -> {
val wasSerializeNulls = writer.serializeNulls
writer.serializeNulls = false
try {
writer.nullValue()
} finally {
writer.serializeNulls = wasSerializeNulls
}
}
is Optional.Present -> valueAdapter.serializeNulls().toJson(writer, value.value)
}
}

Why does my Gson object keep returning null?

I'm trying to parse JSON data to a class but gson.fromJson(response, bitt::class.java) keeps returning null.
class bitt(#SerializedName("result")val result: String) {
val someVal: String = "string"
fun method() {
print("something")
}
}
val response: String = "{'success':true,'message':'','result':'Im a sult'}"
println(response)
val gson = Gson()
val ticker = gson.fromJson(response, bitt::class.java)
println(ticker)
What am I doing wrong here?
JSON always uses double quotes ", not single quotes '. Your response uses single quotes, so it is not valid JSON.
As in many other languages, you can use \" to put a double quote in a string literal:
val response: String = "{\"success\":true,\"message\":\"\",\"result\":\"I'm a result\"}"
change to Data Class instead of Class
example from your code:
data class bitt(val result: String = "") {
val someVal: String = "string"
fun method() {
print("something")
}
}
I guess it takes long time before you get the result back
so the ticker still remain null
you can use kotlin coroutines to handle it.
or simply use callback like this
data class bitt(val result: String = "") {
val someVal: String = "string"
fun method() {
print("something")
}
}
fun getTicker(response: String, onComplete: (bitt) -> Unit) {
val ticker = Gson().fromJson(response, bitt::class.java)
onComplete(ticker)
}
val response: String = "{'success':true,'message':'','result':'Im a sult'}"
println(response)
getTicker(response){ println(it) }
then you might need to use Coroutine
https://github.com/Kotlin/kotlinx.coroutines
data class bitt(val result: String = "") {
val someVal: String = "string"
fun method() {
print("something")
}
}
suspend fun getTicker(response: String) = Gson().fromJson(response, bitt::class.java)
fun yourMethod() {
val response: String = "{'success':true,'message':'','result':'Im a sult'}"
println(response)
CoroutineScope(IO).launch {
val ticker = getTicker(response)
println(ticker)
}
}
KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov

Scala : Retry with try/catch for exception handling

I am trying to add a retry logic for JSON conversion. When converting an object to json, I am retrying for 3 times if there is any exception. I am doing :
var mapper = new ObjectMapper() with ScalaObjectMapper
intializeMapper( )
def intializeMapper() = {
// jackson library does not support seralization and deserialization of
// of scala classes like List and Map, this is needed to support it
mapper.registerModule( DefaultScalaModule )
// enables parsing of NaN. Enabling it here as JsonUtil class currently in
// use supports it.
mapper.configure(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, true )
mapper.setSerializationInclusion(Include.NON_NULL)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
def getPersonRDD(result: DataFrame): RDD[(String, String)] = {
val finalValue = result.rdd.map({
r =>
val customerId = r.getAs[String](CUSTOMER_ID)
val itemId = r.getAs[Map[String, Int]](ITEM_ID)
val itemName = r.getAs[Map[String, Int]](ITEM_NAME)
val person = Person(itemId, itemName)
val jsonString = toJson(person)
(customerId, jsonString)
})
return finalValue
}
def fromJson(json: String, clazz: Class[_]) = {
mapper.readValue(json, clazz)
}
def toJson(value: Any): String = {
var jsonString: String = " "
jsonString = mapper.writeValueAsString(value)
try {
fromJson(jsonString, clazz)
return jsonString
} catch {
case Exception => {
publishMetrics(PARSING_EXCEPTION, 1.0)
val result = util.Try(retry() {
jsonString = mapper.writeValueAsString(value)
val features = fromJson(jsonString, clazz)
})
result match {
case util.Success(value) => jsonString
case util.Failure(error) => {
log.error("Error while parsing JSON " + jsonString)
return jsonString
}
}
}
}
}
// Returning T, throwing the exception on failure
#annotation.tailrec
def retry[T](n: Int = 3)(fn: => T): T = {
util.Try {
fn
} match {
case util.Success(x) => x
case _ if n > 1 => retry(n - 1)(fn)
case util.Failure(e) => throw e
}
}
case class Person(itemId: Map[String, Int], itemName: Map[String, Int]) extends Serializable
Is this correct ? I am new to Scala. Can someone suggest me if there is any better way for achieving this ? Is there predefined retry logic available in Scala ? The reason I am trying to add retry logic for JSON conversion is due to Jackson version I use(which I can't change for now), sometimes my writeValueAsString results in incomplete JSON.
You retry function seems correct. The only flaw I can think of is that if you expect something would fail it's better just make the return type Try[T], so you can handle it outside in the scala way.
Here is one of my implementation:
def retry[T](n: Int)(block: => T): Try[T] = {
val stream = Stream.fill(n)(Try(block))
stream find (_.isSuccess) getOrElse stream.head
}