Maximizing Efficiency: Automatic Data Parsing in React Native

Suson Thapa
6 min readMar 21, 2023

--

Photo by Jean-Philippe Delberghe on Unsplash

If you have written some native code on ReactNative then you know that passing information between Javascript and the native side is essential.

Here is the sample repository that we will be using for demonstration.

Passing objects the usual way

Let’s assume you are passing a Javascript object to the native side and the native side returns the same object back to you(We will be using Swift and Kotlin for the native side).

const response = await TestModule.testMap({
arg: "Button Clicked",
value: {
test: "Button Clicked",
},
});
console.log(response)

This data gets converted to JSON and the native side will convert it back into some form of collection like ReadableMap on Android and Dictionary on iOS.

Android

On Android, to get the object passed from JS you will have to read the properties from the ReadableMap one by one and then construct your Kotlin object from it.

To pass the Kotlin object to the JS side you will have to do the above step in reverse. First, create the ReadableMap and fill it with values from the Kotlin object. Here is how we usually do it.

// native model
data class TestMapArg(
val arg: String,
val value: Value,
) {
data class Value(
val test: String,
)
}

// module function
@ReactMethod
fun testMap(param: ReadableMap, promise: Promise) {
// convert to Kotlin object
val arg = param.getString("arg")!!
val valueMap = param.getMap("value")!!
val test = valueMap.getString("test")!!

val testMapArg = TestMapArg(
arg,
TestMapArg.Value(test),
)
Log.d(TestModule.TAG, "testMap: $testMapArg")

// convert to JS object
val readableMap = Arguments.createMap()
readableMap.putString("arg", testMapArg.arg)

val readableValueMap = Arguments.createMap()
readableValueMap.putString("test", testMapArg.value.test)
readableMap.putMap("value", readableValueMap)

promise.resolve(readableMap)
}

iOS

Let’s see what that code looks like on iOS.

// Native model
struct TestMapArg {
let arg: String
let value: Value

struct Value {
let test: String
}
}

// Module function
@objc func testMap(_ param: [String: Any], _ resolve: RCTPromiseResolveBlock, _ reject: RCTPromiseRejectBlock) {
// convert to Swift object
let arg = param["arg"] as! String
let valueMap = param["value"] as! [String: Any]
let test = valueMap["test"] as! String

let testMapArg = TestMapArg(arg: arg, value: TestMapArg.Value(test: test))
NSLog("testMap: \(testMapArg)")

// convert to JS object
var readableMap: [String: Any] = [:]
readableMap["arg"] = testMapArg.arg

var readableValueMap: [String: Any] = [:]
readableValueMap["test"] = testMapArg.value.test
readableMap["value"] = readableValueMap

resolve(readableMap)
}

This process is very tedious and error-prone.

Isn’t there an easy way to do this?

Wouldn’t it be great if there was some way to convert the Javascript objects directly to native objects and vice-versa? Well, we can. Here is the basic idea.

We will convert the native JS Object (ReadableMap or Dictionary) to plain JSON and then build a native object from it and vice-versa.

Android

On Android, we have libraries like Moshi and GSON that converts JSON to Java/Kotlin objects. We will be using Moshi.

Let’s start with initializing Moshi and defining a Codable interface which is just a marker that we will use to build extension functions. This is very similar to the Codable protocol on iOS.

interface Codable

object MoshiParser {
private val moshi = Moshi.Builder().build()

fun <T> fromJsonString(json: String, type: Type): T? {
return moshi.adapter<T>(type).fromJson(json)
}

fun <T> toJsonString(obj: T, type: Type): String? {
return moshi.adapter<T>(type).toJson(obj)
}
}

Let’s mark our Kotlin model with the Codable interface and annotation from Moshi.

@JsonClass(generateAdapter = true)
data class TestMapArg(
val arg: String,
val value: Value,
) : Codable {
@JsonClass(generateAdapter = true)
data class Value(
val test: String,
) : Codable
}

Here is an extension function on ReadableMap that is going to convert it to a Kotlin object.

inline fun <reified T> ReadableMap.decode(): T {
val hashMap = toHashMap()
val jsonString = MoshiParser.toJsonString(hashMap, Map::class.java)!!
return MoshiParser.fromJsonString<T>(jsonString, T::class.java)!!
}

Here is another extension function to convert the Kotlin object to ReadableMap.

fun Codable.encode(): ReadableMap {
val json = MoshiParser.toJsonString(this, this::class.java)!!
val hashMap = MoshiParser.fromJsonString<Map<String, *>>(json, Map::class.java)
return Arguments.makeNativeMap(hashMap)
}

With this, the implementation of the testMap function looks like this, very simple.

@ReactMethod
fun testMap(param: ReadableMap, promise: Promise) {
// convert the JS object to kotlin object
val testMapArg = param.decode<TestMapArg>()
Log.d(TestModule.TAG, "testMap: argument -> $testMapArg")

// convert the kotlin object to JS object
val readableMap = testMapArg.encode()
promise.resolve(readableMap)
}

Isn’t this awesome?

What about the performance? I don’t think this will cause any significant performance issues unless you are passing really big objects every millisecond.

iOS

On iOS, things are a bit simpler. They already have the Codable protocol for the JSON conversion so we can utilize that.

Let’s mark the Swift model with the Codable protocol. To make the code consistent with Android I have added a type alias named ReadableMap which is just a simple Dictionary.

typealias ReadableMap = [String: Any]

struct TestMapArg: Codable {
let arg: String
let value: Value

struct Value: Codable {
let test: String
}
}

Here is an extension function to convert the ReadableMap into a Swift struct.

extension ReadableMap {
func decode<T: Codable>() throws -> T {
let json = try JSONSerialization.data(withJSONObject: self)
return try JSONDecoder().decode(T.self, from: json)
}
}

Similarly, here is an extension function to convert Swift struct to ReadableMap.

extension Encodable {
func encode() throws -> ReadableMap {
let data = try JSONEncoder().encode(self)
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! ReadableMap
}
}

Now, the testMap will be as simple as this.

@objc func testMap(_ param: ReadableMap, _ resolve: RCTPromiseResolveBlock, _ reject: RCTPromiseRejectBlock) {
do {
// convert the JS object to Swift object
let testMapArg: TestMapArg = try param.decode()
NSLog("testMap: argument -> \(testMapArg)")

// convert the Swift object to JS object
let readableMap = try testMapArg.encode()
resolve(readableMap)
} catch {}
}

What about arrays?

I’m glad you asked. It is quite simple to implement.

Android

Here is an extension function to do the conversion between Kotlin and JS array.

// JS array to Kotlin List
inline fun <reified T> ReadableArray.decode(): List<T> {
val arrayList = toArrayList()
val jsonString = MoshiParser.toJsonString(arrayList, List::class.java)!!
return MoshiParser.fromJsonString(
jsonString,
Types.newParameterizedType(List::class.java, T::class.java)
)!!
}

// Kotlin collection to JS array
fun Collection<Codable>.encode(): ReadableArray {
val json = MoshiParser.toJsonString(this, List::class.java)!!
val array = MoshiParser.fromJsonString<List<*>>(json, List::class.java)
return Arguments.makeNativeArray(array)
}

Since Codable is something we created this will not work with an array of primitive types like string, integer, etc. So, we have to overload the encode method for this. We could have gotten away with using a wildcard like this.

fun Collection<*>.encode(): ReadableArray {
val json = MoshiParser.toJsonString(this, List::class.java)!!
val array = MoshiParser.fromJsonString<List<*>>(json, List::class.java)
return Arguments.makeNativeArray(array)
}

But this will make the extension function available for collections with any type of object. So, we are using the following to limit the extension function to Codable and primitive data types.

@JvmName("encodeCodable")
fun Collection<Codable>.encode(): ReadableArray {
return encodeInternal(this)
}

@JvmName("encodeNumber")
fun Collection<Number>.encode(): ReadableArray {
return encodeInternal(this)
}

@JvmName("encodeBoolean")
fun Collection<Boolean>.encode(): ReadableArray {
return encodeInternal(this)
}

@JvmName("encodeString")
fun Collection<String>.encode(): ReadableArray {
return encodeInternal(this)
}

private fun encodeInternal(args: Collection<*>): ReadableArray {
val json = MoshiParser.toJsonString(args, List::class.java)!!
val array = MoshiParser.fromJsonString<List<*>>(json, List::class.java)
return Arguments.makeNativeArray(array)
}

iOS

Things are a bit simpler on iOS as always. The primitive types already conform to Codable so we don’t have to do anything. To make the code consistent let's add another type alias for ReadableArray.

typealias ReadableArray = [Any]

// JS array to Swift array
extension ReadableArray {
func decode<T: Codable>() throws -> [T] {
let json = try JSONSerialization.data(withJSONObject: self)
return try JSONDecoder().decode([T].self, from: json)
}
}

// Swift array to JS array
extension Array where Element: Codable {
func encode() throws -> ReadableArray {
let data = try JSONEncoder().encode(self)
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! ReadableArray
}
}

You can look into the sample repository to see how we can pass primitive arrays.

Well, that’s it for this story. Catch you in the next one, peace!

--

--

Suson Thapa
Suson Thapa

Written by Suson Thapa

Android | iOS | Flutter | ReactNative — Passionate Software Engineer

No responses yet