How to parse JSON with N child objects - json

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).

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.

Kotlin reading json unknown type Spring

I'm calling different APIs, that use the same key name in the JSON file. Depending on the response, there's one field that may be different types.
To be clear:
The key "results" when calling the API nº1 is a JSON object
The key "results" when calling the API nº2 is a JSON array
My code looks like this when using the second API:
data class Result(
#SerializedName("results") var persons:ArrayList<Person> =ArrayList()
)
The question is if there's any way to use the same class, without taking care if it's a JSON array or a JSON object.
I believe you can define results as an instance of com.fasterxml.jackson.databind.JsonNode.
data class Result(
val results: JsonNode
)
Then you can process results based on it's type—whether it is an ArrayNode or an ObjectNode (as both extend JsonNode):
fun processResults(results: JsonNode) = when{
results.isArray -> processArrayNode(results)
else -> processObjectNode(results)
}
private fun processArrayNode(list: JsonNode): *return whatever you need*{
val elements = list
.elements()
.asSequence()
.toList()
val mappedElements = elements.map{
processObjectNode(it)
}
// do whatever you need with the array
}
private fun processObjectNode(person: JsonNode): *return whatever you need*{
//** this will transform the json node into a linkedHashMap where the keys are the json keys and the values are the values (here interpreted as jsonNodes) **/
val fieldsMap = person
.fields()
.asSequence()
.associateBy( {it.key}, {it.value} )
// process whatever you need
}
This is one way to use the same DTO for both API calls. In my opinion, it is not worth the extra work. I would create two DTOs containing the results field, where in one it is an instance of Person, and in the other it is an instance of List<Person>.
Edit: One little upgrade to the above snippet would be to add extension methods to JsonNode:
fun JsonNode.elementsToList(): List<JsonNode> = this
.elements()
.asSequence()
.toList()
fun JsonNode.fieldsToMap(): Map<String, JsonNode> = this
.fields()
.asSequence()
.associateBy({it.key}, {it.value})
You can use ObjectMapper.typeFactory.constructParametricType to handle generic types:
data class Result<T>(
var x:T
)
val om = ObjectMapper()
om.registerModule(KotlinModule())
val parsedList = om.readValue<Result<List<String>>>(
"""{"x":["x1", "x2"]}""",
om.typeFactory.constructParametricType(Result::class.java, List::class.java)
)
println(parsedList)
val parsedMap = om.readValue<Result<Map<String, String>>>(
"""{"x":{"k1": "v1", "k2": "v2"}}""",
om.typeFactory.constructParametricType(Result::class.java, Map::class.java)
)
println(parsedMap)
Gives output:
Result(x=[x1, x2])
Result(x={k1=v1, k2=v2})

Kotlinx.Serialization using OkHTTPClient return always Failure

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")
}
}

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.