Read large (+- 50Mb) Json file using Kotlin - json

I'm starting to work on a weather app using "openweathermap.org" API, and they provide you with a list of available cities in Json format.
Before i continue with the project, i would like to able to work with the data from this Json file.
The problem is that i get Null whenever i try to read and parse that file.
Here is the code:
Main Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val jsonFileString = getJsonDataFromAsset(applicationContext, "citylist.json")
Log.i("gabs Data", jsonFileString ?: "Empty Data")
val gson = Gson()
val listOfCities = object : TypeToken<List<CityList>>() {}.type
var cities: List<CityList> = gson.fromJson(jsonFileString, listOfCities)
cities.forEachIndexed { idx, city -> Log.i("data", "> Item $idx:\n$city") }
}
}
Utils.kt:
fun getJsonDataFromAsset(context: Context, fileName: String): String? {
val jsonString: String
try {
jsonString = context.assets.open(fileName).bufferedReader().use { it.readText() }
} catch (ioException: IOException) {
ioException.printStackTrace()
return null
}
return jsonString
}
And the data class (Array of cities data):
class CityList : ArrayList<CityList.CityListItem>(){
data class CityListItem(
#SerializedName("coord")
val coord: Coord,
#SerializedName("country")
val country: String,
#SerializedName("id")
val id: Double,
#SerializedName("name")
val name: String,
#SerializedName("state")
val state: String
) {
data class Coord(
#SerializedName("lat")
val lat: Double,
#SerializedName("lon")
val lon: Double
)
}
}
And the error:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.weatherdisplay/com.example.weatherdisplay.ui.activities.MainActivity}: java.lang.NullPointerException: gson.fromJson(jsonFileString, listOfCities) must not be null.
Caused by: java.lang.NullPointerException: gson.fromJson(jsonFileString, listOfCities) must not be null
at com.example.weatherdisplay.ui.activities.MainActivity.onCreate(MainActivity.kt:21)

There were some problems in your code:
You were not closing the BufferedReader
You should not load the file on the Main thread since it will block the UI
I created some sample data corresponding to your data structure:
[
{
"id": 1,
"country": "Germany",
"state": "Saxony",
"name": "Dresden",
"coord": {
"lat": 0.0,
"lon": 0.0
}
},
{
"id": 2,
"country": "Germany",
"state": "Berlin",
"name": "Berlin",
"coord": {
"lat": 0.0,
"lon": 0.0
}
},
{
"id": 3,
"country": "Germany",
"state": "Baden-Wuerttemberg",
"name": "Stuttgart",
"coord": {
"lat": 0.0,
"lon": 0.0
}
},
{
"id": 4,
"country": "Germany",
"state": "Hessen",
"name": "Frankfurth",
"coord": {
"lat": 0.0,
"lon": 0.0
}
},
{
"id": 5,
"country": "Germany",
"state": "Nordrhine-Westphalia",
"name": "Cologne",
"coord": {
"lat": 0.0,
"lon": 0.0
}
}
]
Your activity:
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MyApplication"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launchWhenStarted {
launch(Dispatchers.IO) {
var reader: BufferedReader? = null
try {
// Create a reader and read the file contents
reader = assets.open("data.json").bufferedReader()
val rawData = reader.use { it.readText() }
// Create a Type token that Gson knows how to parse the raw data
val cityListType = object : TypeToken<List<City>>() {}.type
// Parse the raw data using Gson
val data: List<City> = Gson().fromJson(rawData, cityListType)
// TODO: Do something with the data
} catch (e: IOException) {
// Handle IOException: Gets thrown when the file wasn't found or something similar
Log.e(TAG, "An error occurred while reading in the data:", e)
} catch (e: JsonParseException) {
// Handle JsonParseException: Gets thrown when there is a problem with the contents of the file
Log.e(TAG, "An error occurred while reading in the data:", e)
}
finally {
// Close the reader to release system resources
reader?.close()
}
}
}
}
}
Your data structure:
data class City(
#SerializedName("id")
val id: Int,
#SerializedName("country")
val country: String,
#SerializedName("state")
val state: String,
#SerializedName("name")
val name: String,
#SerializedName("coord")
val coordinate: Coordinate
) {
override fun toString(): String {
return "#$id[$name $state $country]#[${coordinate.lat}|${coordinate.lon}]"
}
}
data class Coordinate(
#SerializedName("lat")
val lat: Double,
#SerializedName("lon")
val lon: Double
)
In the best case you would put the code in which you get the file contents and parse the data in a ViewModel, but this would to go beyond the scope for this answer.
Additional information about ViewModels: https://developer.android.com/topic/libraries/architecture/viewmodel

Related

Kotlin: How to serialize a interface and ignore the class attributes

interface MyInterface {
fun schemaName(): String
fun data(): Any
}
data class Object1(
val name: String,
val title: String,
val description: String,
val list: List<String>
): MyInterface {
override fun schemaName(): String = "SCHEMA_FOR_OBJECT1"
override fun data(): Any = this
}
With this MyInterface and Object1, Kotlin serializes my Object1 into
{
"name": xxx,
"title": xxxx,
"description": xxxx,
"list": [
"xxxx",
"xxxx"
]
}
This is understandable but is there anyway I can serialize Object1 into
{
"schemaName": "SCHEMA_FOR_OBJECT1",
"data": {
{
"name": xxx,
"title": xxxx,
"description": xxxx,
"list": [
"xxxx",
"xxxx"
]
}
}
}
I want to have multiple objects which implement MyInterface but I want them all to serialize into a json with common parent keys.

How to serialize fields with varying type?

I have the following data classes to parse JSON. I can parse it easily with the decodeFromString method. However, the Info classes could contain the List<Int> type from time to time along with the Int type so that both are included in a single JSON. How can I handle this variation in serialization?
#Serializable
data class Node (#SerialName("nodeContent") val nodeContent: List<Info>)
#Serializable
data class Info (#SerialName("info") val info: Int)
p.s. The closest question to mine is this one: Kotlinx Serialization, avoid crashes on other datatype. I wonder if there are other ways?
EDIT:
An example is given below.
"nodeContent": [
{
"info": {
"name": "1",
},
},
{
"info": [
{
"name": "1"
},
{
"name": "2"
},
],
},
{
"info": {
"name": "2",
},
}
]
Here is an approach with a custom serializer similar to the link you provided. The idea is to return a list with just a single element.
// Can delete these two lines, they are only for Kotlin scripts
#file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0")
#file:CompilerOptions("-Xplugin=/snap/kotlin/current/lib/kotlinx-serialization-compiler-plugin.jar")
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.Decoder
#Serializable
data class Node (val nodeContent: List<Info>)
#Serializable(with = InfoSerializer::class)
data class Info (val info: List<Name>)
#Serializable
data class Name (val name: Int)
#Serializer(forClass = Info::class)
object InfoSerializer : KSerializer<Info> {
override fun deserialize(decoder: Decoder): Info {
val json = ((decoder as JsonDecoder).decodeJsonElement() as JsonObject)
return Info(parseInfo(json))
}
private fun parseInfo(json: JsonObject): List<Name> {
val info = json["info"] ?: return emptyList()
return try {
listOf(Json.decodeFromString<Name>(info.toString()))
} catch (e: Exception) {
(info as JsonArray).map { Json.decodeFromString<Name>(it.toString()) }
}
}
}
Usage:
val ss2 = """
{
"nodeContent": [
{
"info":
{"name": 1}
},
{
"info": [
{"name": 1},
{"name": 2}
]
},
{
"info":
{"name": 2}
}
]
}
"""
val h = Json.decodeFromString<Node>(ss2)
println(h)
Result:
Node(nodeContent=[Info(info=[Name(name=1)]), Info(info=[Name(name=1), Name(name=2)]), Info(info=[Name(name=2)])])

initialize json file data as a TypeScript Map

as a young typescript padawan
i am trying to parse mock data from a json witch involves initializing a Map<string, MenuPageData>
and i get an error (attached below)
i would like to be able to supply the correct data format in the json or map it properly from the existing data.
here is the error:
here is the code:
export interface MenuItem {
data: {
id: string;
name: string;
currency?: string;
imageUrl?: string;
description?: string;
price?: number;
};
}
export interface MenuPageData {
pageName: string;
menuItems: MenuItem[];
}
export interface MenuPageDataCollection {
menuPages: Map<string, MenuPageData>;
}
the error is comming from "(pagesDataMock.menuPages)"
const dataCollection: MenuPageDataCollection = { menuPages: new Map<string, MenuPageData>(pagesDataMock.menuPages) };
export const menuPagesCollection: () => MenuPageDataCollection = (): MenuPageDataCollection => {
return dataCollection;
};
and here is the json source:
{
"menuPages": [
"mealPage1",
{
"pageName": "menuPage1",
"menuItems": [
{
"data": {
"id": "null0",
"name": "meal",
"currency": "EUR",
"imageUrl": "../images/greenHand.jpg",
"description": "tasty",
"price": 12
}
},
{
"data": {
"id": "null0",
"name": "meal",
"currency": "EUR",
"imageUrl": "../images/greenHand.jpg",
"description": "tasty",
"price": 12
}
}
]
},
"mealPage2",
{
"pageName": "menuPage1",
"menuItems": [
{
"data": {
"id": "null0",
"name": "meal",
"currency": "EUR",
"imageUrl": "../images/greenHand.jpg",
"description": "tasty",
"price": 12
}
},
{
"data": {
"id": "null0",
"name": "meal",
"currency": "EUR",
"imageUrl": "../images/greenHand.jpg",
"description": "tasty",
"price": 12
}
}
]
}
]
}
i would appreciate and feedback, i tried several different ways of doing it but now 3-4 days passed by and i am running on empty of understanding
Map is a rather strong constraint that is not representable in JSON as is. First you'll have to validate the input is satisfying the requirements for your Map<string, MenuPageData>. Then to make it understandable by TS wrap in into type guard or type assert function. And finally convert it into the actual Map object yourself.
Simplified example:
interface IFace {
prop: string
}
interface Res {
title: string
ifaceMap: Map<string, IFace>
}
interface ResJSON {
title: string
ifaceMap: { [K in string]: IFace }
}
function assertResponse(json: unknown): asserts json is ResJSON {
...
}
function convert2Res(res: ResJSON): Res {
const { title, ifaceMap } = res
const map = new Map()
for(let key in ifaceMap) map.set(key, ifaceMap[key])
return { title, ifaceMap: map }
}
assertResponse(goodResponse)
const mapRes = convert2Res(goodResponse)
TS playground
In case you have full control of the source json and absolutely sure it may never be misshaped you may completely skip the assert part and feed the response (forcibly asserted to ResJSON type) right into convert2Res.

Unable to create converter for class when using sealed class or an interface with Moshi

I am trying to parse a json data from a server.It has dynamic keys so I am trying to have like a parent class that have the shared keys and child class for each specific node. I wrote a kotlin code using retrofit and Moshi but it's not working. I tried with a sealed class and interface without success. Actually I would prefer that works with sealed class but I don't know what I am doing wrong
interface MyApi {
#GET("/...")
fun fetchMyFeed(): Call<MyResponse>
}
data class MyResponse(
val data: List<ParentResponse>
)
interface ParentResponse{
val name: String
}
data class Child1Response(
val age: String,
val kids: List<KidsResponse>,
val cars: List<CarsResponse>
)
data class Child2Response(
val job: String,
val address: List<AddressResponse>
)
fun fetchAllFeed(): List<Any>? =
try {
val response = api.fetchMyFeed().execute()
if (response.isSuccessful) {
Log.d("check",${response.body()?.data?})
null
} else null
} catch (e: IOException) {
null
} catch (e: RuntimeException) {
null
}```
and the json file is :
{
"data": [
{
"name": "string",
"job": "string",
"address": [
{
"avenue": "string",
"imageUrl": "string",
"description": "string"
}
]
},
{
"name": "string",
"age": "string",
"kids": {
"count": "string",
"working": "string"
},
"cars": [
{
"brand": "string",
"age": "string",
"imageUrl": "string"
}
]
}
]
}
Unable to create converter for class
You can make use of JsonAdapter from moshi to parse different JSON Models if you can differentiate them by foreseeing some value in the json.
for example, consider json response having two schemas,
{
"root": {
"subroot": {
"prop" : "hello",
"type" : "String"
}
}
}
(or)
{
"root": {
"subroot": {
"prop" : 100,
"type" : "Integer"
}
}
}
Here, subroot has different schemas (one containing string property and another containg a integer property) which can be identified by "type"
You can create a parent sealed class with common keys and derive few child classes with varying keys. Write a adapter to select the type of class to be used while json serialization and add that adapter to moshi builder.
Model classes:
class Response {
#Json(name = "root")
val root: Root? = null
}
class Root {
#Json(name = "subroot")
val subroot: HybridModel? = null
}
sealed class HybridModel {
#Json(name = "type")
val type: String? = null
class StringModel : HybridModel() {
#Json(name = "prop")
val prop: String? = null
}
class IntegerModel : HybridModel() {
#Json(name = "prop")
val prop: Int? = null
}
}
Few extension methods to JsonReader,
inline fun JsonReader.readObject(process: () -> Unit) {
beginObject()
while (hasNext()) {
process()
}
endObject()
}
fun JsonReader.skipNameAndValue() {
skipName()
skipValue()
}
HybridAdapter to select type of class for "subroot" key
class HybridAdapter : JsonAdapter<HybridModel>() {
#FromJson
override fun fromJson(reader: JsonReader): HybridModel {
var type: String = ""
// copy reader and foresee type
val copy = reader.peekJson()
copy.readObject {
when (copy.selectName(JsonReader.Options.of("type"))) {
0 -> {
type = copy.nextString()
}
else -> copy.skipNameAndValue()
}
}
//handle exception if type cannot be identified
if (type.isEmpty()) throw JsonDataException("missing type")
// build model based on type
val moshi = Moshi.Builder().build()
return if (type == "String")
moshi.adapter(HybridModel.StringModel::class.java).fromJson(reader)!!
else
moshi.adapter(HybridModel.IntegerModel::class.java).fromJson(reader)!!
}
#ToJson
override fun toJson(p0: JsonWriter, p1: HybridModel?) {
// serialization logic
}
}
Finally build Moshi with the HybridAdapter to serialize HybridModel,
fun printProp(response: Response?) {
val subroot = response?.root?.subroot
when (subroot) {
is HybridModel.StringModel -> println("string model: ${subroot.prop}")
is HybridModel.IntegerModel -> println("Integer model: ${subroot.prop}")
}
}
fun main() {
val jsonWithStringSubroot =
"""
{
"root": {
"subroot": {
"prop" : "hello",
"type" : "String"
}
}
}
"""
val jsonWithIntegerSubroot =
"""
{
"root": {
"subroot": {
"prop" : 1,
"type" : "Integer"
}
}
}
"""
val moshi = Moshi.Builder().add(HybridAdapter()).build()
val response1 = moshi.adapter(Response::class.java).fromJson(jsonWithStringSubroot)
printProp(response1) // contains HybridModel.StringModel
val response2 = moshi.adapter(Response::class.java).fromJson(jsonWithIntegerSubroot)
printProp(response2) // contains HybridModel.IntegerModel
}

How to parse just part of JSON with Klaxon?

I'm trying to parse some JSON to kotlin objects. The JSON looks like:
{
data: [
{ "name": "aaa", "age": 11 },
{ "name": "bbb", "age": 22 },
],
otherdata : "don't need"
}
I just need to data part of the entire JSON, and parse each item to a User object:
data class User(name:String, age:Int)
But I can't find an easy way to do it.
Here's one way you can achieve this
import com.beust.klaxon.Klaxon
import java.io.StringReader
val json = """
{
"data": [
{ "name": "aaa", "age": 11 },
{ "name": "bbb", "age": 22 },
],
"otherdata" : "not needed"
}
""".trimIndent()
data class User(val name: String, val age: Int)
fun main(args: Array<String>) {
val klaxon = Klaxon()
val parsed = klaxon.parseJsonObject(StringReader(json))
val dataArray = parsed.array<Any>("data")
val users = dataArray?.let { klaxon.parseFromJsonArray<User>(it) }
println(users)
}
This will work as long as you can fit the whole json string in memory. Otherwise you may want to look into the streaming API: https://github.com/cbeust/klaxon#streaming-api