Kotlinx.Serialization using OkHTTPClient return always Failure - json

Hello I have a problem with my JSON. I am using OkHTTPClient to get JSON from web - to get objects from JSON using kotlinx.serialization via method which contains this and return value from method should be Result :
private suspend inline fun <reified T> OkHttpClient.get(webUrl: HttpUrl): Result<T> =
try {
//Builder defined here ... but skipping this line of code
val data = Json { ignoreUnknownKeys = true }.decodeFromString<T (result.body!!.string())
Result.Success(data)
} catch (e: Exception) {
Result.Failure(e)
}
suspend fun getFact(): Result<Fact> =
client.httpGet("myURL".toHttpUrl())
Json from myURL:
{"status":"success","data":[{"fact":"This is random information i need to get"}],"message":"Retrieved Fact"}
My serializer and Serializable data classes:
#Serializable
data class Fact(
#Serializable(with = FactListSerializer::class)
val data: String) java.io.Serializable
object FactListSerializer : JsonTransformingSerializer<List<String>>(ListSerializer(String.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return if (element is JsonArray) {
JsonArray(listOf(element)).first()
} else {
element
}
}
}
To be honest I am not sure what I am doing, but I am getting this error all the time when I print val fact = api.getFact():
Fact: Failure(error=kotlinx.serialization.json.internal.JsonDecodingException: Expected JsonPrimitive at 0, found {"fact":"This is random information i need to get"}
What I need to return is only first element of array fact, because JSON obtain always only 1 fact inside array. So I don't want to return from Serializer/Json List but only Fact object.
But as you see I am obtaining always Result Fauilure, don't know why. My goal is to obtain Result Success and obtaining from that JSON object Fact (only one), but I am not sure if I am doing it correct (obviously not) and even if it is even possible to return from JSONArray only one object (element of type Fact).
So what I expect is something like this:
Fact: Success(value=Fact(fact=This is random information i need to get))

I think the deserializer definition should be changed on 3 levels. The example of how to use JsonTransformingDeserializer in the docs actually describes most of what you need.
JsonArray(listOf(element)).first() should just be element.first(). Here you're building a JsonArray containing your initial JsonArray as only element, and then taking the first, so you basically get back the exact same element.
The type parameter T of JsonTransformingSerializer is supposed to be the type of the property it's applied to, so you should at least get a warning in the code because yours is defined to work on List<String> but is applied to a String property. It should be JsonTransformingSerializer<String>(String.serializer()).
You not only need to unwrap the data array, you also need to extract the value of the fact key within the element of that array.
So with all these changes, it should give something like this:
object FactListSerializer : JsonTransformingSerializer<String>(String.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val unwrappedData = if (element is JsonArray) element.first() else element
return unwrappedData.jsonObject["fact"] ?: error("missing 'fact' key in 'data' array")
}
}

Related

How to serialize a Kotlin Set to a JSON object whose values are empty objects?

I would like to make a Kotlin class serializable using Kotlin serialization.
The class is very simple, something like this:
#Serializable(with = CustomSerializer::class)
data class MyObject(val keys: Set<String>)
Now, I need the serialization format to be a JSON object where the keys are given by the Set<String> and the values are always empty JSON objects.
Example:
val example = MyObject(setOf("abc", "def"))
Should serialize to:
{ "abc": {}, "def": {} }
The reason is that this object is being sent to an API where that's how they want the JSON to look like... the empty objects could contain some directives but I don't want or need to use those.
Having trouble doing that by just reading the documentation.
I've found one way to do it... and it seems simple enough!
I realized that I can get a serializer of empty Objects almost for free with this:
#Serializable
private object EmptyMap
Now, I can write a custom serializer in a straightforward way:
object MyObjectSerializer : KSerializer<MyObject> {
private val _delegate = MapSerializer(String.serializer(), EmptyMap.serializer())
override val descriptor: SerialDescriptor = _delegate.descriptor
override fun serialize(encoder: Encoder, value: MyObject) {
val data = value.keys.associateWith { EmptyMap }
encoder.encodeSerializableValue(_delegate, data)
}
override fun deserialize(decoder: Decoder): MyObject {
val value = decoder.decodeSerializableValue(_delegate)
return MyObject(value.keys)
}
}
Now all that's left to do is to apply the serializer on the type, which can be done with:
#Serializer(with = MyObjectSerializer)
data class MyObject(val keys: Set<String>)
Running Json.encodeToString(example) on the examples works perfectly.

Kotlin JSON deserialization with nested class discriminator

I want to use a class discriminator in Kotlin to get polymorphic deserialization of JSON messages. The problem is that the class discriminator in my messages is nested one level deep:
{
"header": {
"message_type": "FooBar"
}
"data": {...}
}
The contents of data depend on message_type. If message_type were one level higher, this would be trivial by setting #JsonClassDiscriminator("message_type") on the base class and #SerialName("FooBar") on the subclass. But since it is nested it is unclear how to solve this without introducing a ton of boilerplate serialization code.
Not the prettiest solution, but you can hack around this as such:
write your #Serializable object as if the discriminator is at the top level
when parsing, first use decodeFromString<JsonElement> to get a generic JSON object
use that object to move the discriminator to top-level
use decodeFromJsonElement to parse that
What step 3 looks like
// if json object has a message type in its header,
// return an object with that at top level so we can use it as a class discriminator
fun hoistDiscriminator(json: JsonElement): JsonElement {
if (json is JsonObject) {
val header = json["header"]
if (header is JsonObject) {
val discriminator = header["message_type"]
if (discriminator is JsonPrimitive && discriminator.isString) {
val map = json.toMutableMap()
map["message_type"] = discriminator
return JsonObject(map)
}
}
}
return json
}
and then the full parsing looks like this
json.decodeFromJsonElement(hoistDiscriminator(json.decodeFromString(response)))
There is a corner case in step 3: what if message_type is already defined at top-level? Handling that is left as an exercise to the reader.

How do I use Moshi to serialize a json string into org.json.JSONObject?

I have a JSON response from my server which is dynamic in nature and I cannot map it into a Kotlin Data Class.
I would like to create a org.json.JSONObject out of it and parse it from there.
I've looked around SO and Moshi's doc but couldn't find any easy way of achieving this.
Any suggestions?
I've stumbled upon the same problem recently. I needed to resend some of the data from one endpoint to another with adding some new stuff to it.
The response from the server looks like this:
{
"someProperty": "value",
"someOtherProperty": "otherValue",
"someDynamicProperty": {
// There may be anything including nested structures, not known beforehand
}
}
Normally Moshi doesn't have built-in support for something like this, but it allows you to build your own adapters and handle the parsing logic.
What you need is define the type that you want to receive as a result:
#JsonClass(generateAdapter = true)
data class CustomResponse(
val someProperty: String,
val someOtherProperty: String,
val someDynamicProperty: JSONObject?
)
Then, you need to create a custom adapter to handle the parsing:
internal object JSONObjectAdapter {
#FromJson
fun fromJson(reader: JsonReader): JSONObject? {
// Here we're expecting the JSON object, it is processed as Map<String, Any> by Moshi
return (reader.readJsonValue() as? Map<String, Any>)?.let { data ->
try {
JSONObject(data)
} catch (e: JSONException) {
// Handle error if arises
}
}
}
#ToJson
fun toJson(writer: JsonWriter, value: JSONObject?) {
value?.let { writer.value(Buffer().writeUtf8(value.toString())) }
}
}
Then, just add this adapter to Moshi builder on creation:
Moshi.Builder().add(JSONObjectAdapter).build()
or use Moshi annotations if you want to apply it only to some particular properties.

How to parse JSON with N child objects

I am trying to parse a JSON response I am receiving from an 3rd party API.
The response returns a json object with N child objects.
The children all conform to the same model/class, but the amount of children can change.
Had it been an array of objects, it would have been trivial, but I am not sure how to do it with an object holding N objects.
I believe I need a custom typeadapter but I can't seem to get it done.
Here is the JSON:
"api":{
"results": 94
"leagues": {
"1":{
"league_id":"1"
"name":"2018 Russia World Cup"
"country":"World"
"season":"2018"
"season_start":"2018-06-14"
}
"2":{...}
"3":{...}
"4":{...}
...
"N":{...}
}
}
So basically it is the "leagues" object I am trying to parse.
I am hoping to end up with a List<League>
For instance, the the root object could have this model:
class Api {
val results: Int
val leagues: List<League>
}
Personally I'd go for a Map<String, League> (assuming the entries in the map would be of class League) for the type of leagues.
class Api {
val results: Int
val leagues: Map<String, League>
}
I think the things to consider here are mostly regarding the order I suppose. If you need to maintain the order of the entries, I'm not sure if Moshi does it automatically or if you need to use a specific implementation of Map to guarantee this.
You can get make the league list in a custom adapter.
data class Api(val results: Int, val leagues: List<League>)
object LeagueListAdapter {
#FromJson fun fromJson(reader: JsonReader, leagueAdapter: JsonAdapter<League>): List<League> {
reader.beginObject()
val result = mutableListOf<League>()
while (reader.hasNext()) {
reader.skipName()
result += leagueAdapter.fromJson(reader)!!
}
reader.endObject()
return result
}
}
Don't forget to add the adapter when building your Moshi instance (Moshi.Builder.add).

Use the non-default constructor with Jerkson?

I need to serialize/deserialize a Scala class with structure something like the following:
#JsonIgnoreProperties(ignoreUnknown = true, value = Array("body"))
case class Example(body: Array[Byte]) {
lazy val isNativeText = bodyIsNativeText
lazy val textEncodedBody = (if (isNativeText) new String(body, "UTF-8") else Base64.encode(body))
def this(isNativeText: Boolean, textEncodedBody: String) = this((if(isNativeText) str.getBytes("UTF-8") else Base64.decode(textEncodedBody)))
def bodyIsNativeText: Boolean = // determine if the body was natively a string or not
}
It's main member is an array of bytes, which MIGHT represent a UTF-8 encoded textual string, but might not. The primary constructor accepts an array of bytes, but there is an alternate constructor which accepts a string with a flag indicating whether this string is base64 encoded binary data, or the actual native text we want to store.
For serializing to a JSON object, I want to store the body as a native string rather than a base64-encoded string if it is native text. That's why I use #JsonIgnoreProperties to not include the body property, and instead have a textEncodedBody that gets echoed out in the JSON.
The problem comes when I try to deserialize it like so:
val e = Json.parse[Example]("""{'isNativeText': true, 'textEncodedBody': 'hello'}""")
I receive the following error:
com.codahale.jerkson.ParsingException: Invalid JSON. Needed [body],
but found [isNativeText, textEncodedBody].
Clearly, I have a constructor that will work...it just is not the default one. How can I force Jerkson to use this non-default constructor?
EDIT: I've attempted to use both the #JsonProperty and #JsonCreator annotation, but jerkson appears to disregard both of those.
EDIT2: Looking over the jerkson case class serialization source code, it looks like a case class method with the same name as its field will be used in the way that a #JsonProperty would function - that is, as a JSON getter. If I could do that, it would solve my problem. Not being super familiar with Scala, I have no idea how to do that; is it possible for a case class to have a user-defined method with the same name as one of its fields?
For reference, here is the code below that leads me to this conclusion...
private val methods = klass.getDeclaredMethods
.filter { _.getParameterTypes.isEmpty }
.map { m => m.getName -> m }.toMap
def serialize(value: A, json: JsonGenerator, provider: SerializerProvider) {
json.writeStartObject()
for (field <- nonIgnoredFields) {
val methodOpt = methods.get(field.getName)
val fieldValue: Object = methodOpt.map { _.invoke(value) }.getOrElse(field.get(value))
if (fieldValue != None) {
val fieldName = methodOpt.map { _.getName }.getOrElse(field.getName)
provider.defaultSerializeField(if (isSnakeCase) snakeCase(fieldName) else fieldName, fieldValue, json)
}
}
json.writeEndObject()
}
Correct me if I'm wrong, but it looks like Jackson/Jerkson will not support arbitrarily nested JSON. There's an example on the wiki that uses nesting, but it looks like the target class must have nested classes corresponding to the nested JSON.
Anyway, if you're not using nesting with your case classes then simply declaring a second case class and a couple implicit conversions should work just fine:
case class Example(body: Array[Byte]) {
// Note that you can just inline the body of bodyIsNativeText here
lazy val isNativeText: Boolean = // determine if the body was natively a string or not
}
case class ExampleRaw(isNativeText: Boolean, textEncodedBody: String)
implicit def exampleToExampleRaw(ex: Example) = ExampleRaw(
ex.isNativeText,
if (ex.isNativeText) new String(ex.body, "UTF-8")
else Base64.encode(ex.body)
)
implicit def exampleRawToExample(raw: ExampleRaw) = Example(
if (raw.isNativeText) raw.textEncodedBody.getBytes("UTF-8")
else Base64.decode(textEncodedBody)
)
Now you should be able to do this:
val e: Example = Json.parse[ExampleRaw](
"""{'isNativeText': true, 'textEncodedBody': 'hello'}"""
)
You could leave the original methods and annotations you added to make the JSON generation continue to work with the Example type, or you could just convert it with a cast:
generate(Example(data): ExampleRaw)
Update:
To help catch errors you might want to do something like this too:
case class Example(body: Array[Byte]) {
// Note that you can just inline the body of bodyIsNativeText here
lazy val isNativeText: Boolean = // determine if the body was natively a string or not
lazy val doNotSerialize: String = throw new Exception("Need to convert Example to ExampleRaw before serializing!")
}
That should cause an exception to be thrown if you accidentally pass an instance of Example instead of ExampleRaw to a generate call.