Retrofit 2 streaming large JSON response - json

I'm trying to parse / process a JSON response while receiving (streaming / chunked). But I cannot get this to work. When I receive JSON I can only process it after the whole response has been received.
If I test the same code with a static file download, this is working correctly.
Retrofit: 2.6.1
OkHTTP 3.12.0
When I download the 5MB file the notice 'Call OK' will be shown immediately and the 'download complete' later on. For the JSON files the 'Call OK' will take a while.
Logger.info("Start download")
val response = fileApi.download5MBFile() // Streaming
val response = dataHubApi.download3MBJson() //Does not stream
val response = articleApi.download10MBJson() // Does not stream
if (response.isSuccessful) {
Logger.info("Call ok")
val input = response.body()?.byteStream()
val buffer = ByteArray(8192)
var size = 0
while (true) {
val read = input!!.read(buffer)
if (read == -1) {
break
}
size += read
//Logger.info("Progress: ${size/1024/1024}mb")
}
Logger.info("Download complete")
} else {
Logger.info("Call not ok")
}
The factory methods for creating the APIs are all the same like this:
fun create(): FileApi {
val logLevel = Level.HEADERS
val retrofit = Retrofit.Builder()
.baseUrl(URL)
.client(getOkHttpClient(logLevel))
.build()
return retrofit.create(FileApi::class.java)
}
private fun getOkHttpClient(logLevel: Level): OkHttpClient {
return OkHttpClient.Builder()
.readTimeout(60L, TimeUnit.SECONDS) // default retrofit value is 10sec
.build()
}
And all the interface are also in the same format:
interface FileApi {
#GET("/5MB.zip")
#Streaming
suspend fun download5MB(): Response<ResponseBody>
}
What can I do to also stream the JSON?

If it is that large, probably it is because it contains a list of several items instead of one or a small chunk of elements.
[
{ item1 },
{ item2 },
...
]
Even if it is something as a firebase json representation you'll have something as:
{
"key1": {element },
"key2": {element },
"key3": { element },
...
}
If you have trouble parsing the complete json, there is a good chance that you'll also face some trouble saving that into a kotlin list, so the best thing would be to directly save it into a DB where you can manage individual objects or pages of objects later on.
So, the trick is to download the JSON as a streamed text that you parse as you stream, and determine when each element is split, so you will only have to process the element as JSON on the text chunk instead of all the text.
so for example with a stack you may start adding braces, but once you reach 0 level stack, you surely has the stream of one element and parse the text in it.

Related

What might cause (JSON ERROR: no value for)?

I have written some code in Kotlin that should retrieve some data for a dictionary app using the JSON Request Object. I can see that the call is made successfully. The website receiving the call shows the data being sent back but I'm not getting anything back in the results object. Logcat is showing this error (E/JSONĀ ERROR: No value for results). I'm not sure where I'm going wrong in extracting the results. Can someone point me in the right direction?
val jsonObjectRequest = JsonObjectRequest(Request.Method.GET, url, null,
{ response ->
try {
val resultsObj = response.getJSONObject("results")
val result: JSONObject = response.getJSONObject("result")
val term = result.getString("term")
val definition = result.getString("definition")
val partOfSpeech = result.getString("partOfSpeech")
val example = result.getString("example")
} catch (ex: JSONException) {
Log.e("JSON ERROR", ex.message!!)
}
},
{ error: VolleyError? -> error?.printStackTrace() })
The JSON
{
"results": {
"result": {
"term": "consistent, uniform",
"definition": "the same throughout in structure or composition",
"partofspeech": "adj",
"example": "bituminous coal is often treated as a
consistent and homogeneous product"
}
}
}
Have you checked the json format? Json Formatter
Here with this code it is valid. You had his character end line in the wrong place.
{
"results":{
"result":{
"term":"consistent, uniform",
"definition":"the same throughout in structure or composition",
"partofspeech":"adj",
"example":"bituminous coal is often treated as a consistent and homogeneous product"
}
}
}

Kotlin unable to request JSON files with more than one directory in URL?

While trying to download and parse JSON files through Kotlin, it kept failing trying to access the document, though trying different (shorter) URLS seemed to work fine
val string = "http://ddragon.leagueoflegends.com/cdn/11.2.1/data/en_GB/champion.json"
val client = OkHttpClient()
val request = Request.Builder().url(string).build()
client.newCall(request).enqueue(object: Callback
{
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
println(body)
}
override fun onFailure(call: Call, e: IOException) {
println("Failed")
}
})
This gives the "Failed" output in the console:failed#1
However, using a shorter URL such as this:
val string = "https://ddragon.leagueoflegends.com/api/versions.json"
Gives the correct output: working #1
Anyone know why and/or a fix to this?
Thanks!
Update:
Trying with a file that is considerably smaller than the first, but includes two directories instead of one:
val string = "http://static.developer.riotgames.com/docs/lol/maps.json"
Still ends up failing leading me to believe it is unable to access the document if it is too nested within directories?

How to change Process[Task, ByteVector] to json object?

I use http4s and scalaz.steam._
def usersService = HttpService{
case req # POST -> Root / "check" / IntVar(userId) =>{
//val jsonobj=parse(req.as[String])
val taskAsJson:String = write[req.body]
Ok(read[Task](taskAsJson))
}
}
For http4s, the request can get body as the type Process[Task, ByteVector]
The Process is scalaz.stream.process class. You can find details on scalaz.
I want to write a Task here can deal with JSON ByteVector and translate into a Map (like HashMap), layer by layer.
I mean, for example:
{
"id":"12589",
"customers":[{
"id": "898970",
"name":"Frank"
...
},
]
}
I do not know how to write the function via scalaz.stream.Process to change the JSON to mapobject.
I want response and to parse the JSON object, returning another refactored JSON object; how can I do it?

Akka HTTP Streaming JSON Deserialization

Is it possible to dynamically deserialize an external, of unknown length, ByteString stream from Akka HTTP into domain objects?
Context
I call an infinitely long HTTP endpoint that outputs a JSON Array that keeps growing:
[
{ "prop": true, "prop2": false, "prop3": 97, "prop4": "sample" },
{ "prop": true, "prop2": false, "prop3": 97, "prop4": "sample" },
{ "prop": true, "prop2": false, "prop3": 97, "prop4": "sample" },
{ "prop": true, "prop2": false, "prop3": 97, "prop4": "sample" },
{ "prop": true, "prop2": false, "prop3": 97, "prop4": "sample" },
...
] <- Never sees the daylight
I guess that JsonFraming.objectScanner(Int.MaxValue) should be used in this case. As docs state:
Returns a Flow that implements a "brace counting" based framing
operator for emitting valid JSON chunks. It scans the incoming data
stream for valid JSON objects and returns chunks of ByteStrings
containing only those valid chunks. Typical examples of data that one
may want to frame using this operator include: Very large arrays
So you can end up with something like this:
val response: Future[HttpResponse] = Http().singleRequest(HttpRequest(uri = serviceUrl))
response.onComplete {
case Success(value) =>
value.entity.dataBytes
.via(JsonFraming.objectScanner(Int.MaxValue))
.map(_.utf8String) // In case you have ByteString
.map(decode[MyEntity](_)) // Use any Unmarshaller here
.grouped(20)
.runWith(Sink.ignore) // Do whatever you need here
case Failure(exception) => log.error(exception, "Api call failed")
}
I had a very similar problem trying to parse the Twitter Stream (an infinite string) into a domain object.
I solved it using Json4s, like this:
case class Tweet(username: String, geolocation: Option[Geo])
case class Geo(latitude: Float, longitude: Float)
object Tweet{
def apply(s: String): Tweet = {
parse(StringInput(s), useBigDecimalForDouble = false, useBigIntForLong = false).extract[Tweet]
}
}
Then I just buffer the stream and mapped it to a Tweet:
val reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(inputStream), "UTF-8"))
var line = reader.readLine()
while(line != null){
store(Tweet.apply(line))
line = reader.readLine()
}
Json4s has full support over Option (or custom objects inside the object, like Geo in the example). Therefore, you can put an Option like I did, and if the field doesn't come in the Json, it will be set to None.
Hope it helps!
I think that play-iteratees-extras must help you. This library allow to parse Json via Enumerator/Iteratee pattern and, of course, don't waiting for receiving all data.
For example, lest build 'infinite' stream of bytes that represents 'infinite' Json array.
import play.api.libs.iteratee.{Enumeratee, Enumerator, Iteratee}
var i = 0
var isFirstWas = false
val max = 10000
val stream = Enumerator("[".getBytes) andThen Enumerator.generateM {
Future {
i += 1
if (i < max) {
val json = Json.stringify(Json.obj(
"prop" -> Random.nextBoolean(),
"prop2" -> Random.nextBoolean(),
"prop3" -> Random.nextInt(),
"prop4" -> Random.alphanumeric.take(5).mkString("")
))
val string = if (isFirstWas) {
"," + json
} else {
isFirstWas = true
json
}
Some(Codec.utf_8.encode(string))
} else if (i == max) Some("]".getBytes) // <------ this is the last jsArray closing tag
else None
}
}
Ok, this value contains jsArray of 10000 (or more) objects. Lets define case class that will be contain data of each object in our array.
case class Props(prop: Boolean, prop2: Boolean, prop3: Int, prop4: String)
Now write parser, that will be parse each item
import play.extras.iteratees._
import JsonBodyParser._
import JsonIteratees._
import JsonEnumeratees._
val parser = jsArray(jsValues(jsSimpleObject)) ><> Enumeratee.map { json =>
for {
prop <- json.\("prop").asOpt[Boolean]
prop2 <- json.\("prop2").asOpt[Boolean]
prop3 <- json.\("prop3").asOpt[Int]
prop4 <- json.\("prop4").asOpt[String]
} yield Props(prop, prop2, prop3, prop4)
}
Please, see doc for jsArray, jsValues and jsSimpleObject. To build result producer:
val result = stream &> Encoding.decode() ><> parser
Encoding.decode() from JsonIteratees package will decode bytes as CharString. result value has type Enumerator[Option[Item]] and you can apply some iteratee to this enumerator to start parsing process.
In total, I don't know how you receive bytes (the solution depends heavily on this), but I think that show one of the possible solutions of your problem.

Grails: Parsing through JSON String using JSONArray/JSONObject

I have the below JSON string coming in as a request parameter into my grails controller.
{
"loginName":"user1",
"timesheetList":
[
{
"periodBegin":"2014/10/12",
"periodEnd":"2014/10/18",
"timesheetRows":[
{
"task":"Cleaning",
"description":"cleaning description",
"paycode":"payCode1"
},
{
"task":"painting",
"activityDescription":"painting description",
"paycode":"payCode2"
}
]
}
],
"overallStatus":"SUCCESS"
}
As you can see, the timesheetList might have multiple elements in it. In this ( above ) case, we have only one. So, I expect it to behave like an Array/List.
Then I had the below code to parse through it:
String saveJSON // This holds the above JSON string.
def jsonObject = grails.converters.JSON.parse(saveJSON) // No problem here. Returns a JSONObject. I checked the class type.
def jsonArray = jsonArray.timesheetList // No problem here. Returns a JSONArray. I checked the class type.
println "*** Size of jsonArray1: " + jsonArray1.size() // Returns size 1. It seemed fine as the above JSON string had only one timesheet in timesheetList
def timesheet1 = jsonArray[1] // This throws the JSONException, JSONArray[1] not found. I tried jsonArray.getJSONObject(1) and that throws the same exception.
Basically, I am looking to seamlessly iterate through the JSON string now. Any help?
1st off to simplify your code, use request.JSON. Then request.JSON.list[ 0 ] should be working