Skip to content

Instantly share code, notes, and snippets.

@jeremy-w
Last active February 25, 2019 16:28
Show Gist options
  • Save jeremy-w/8556689ecbb6e870113ce5c373fec6a5 to your computer and use it in GitHub Desktop.
Save jeremy-w/8556689ecbb6e870113ce5c373fec6a5 to your computer and use it in GitHub Desktop.
Exploring how Moshi reacts to missing required JSON fields, thanks to Arek Olek's prompting
/**
This was a small test bench to see how [Moshi](https://github.com/square/moshi)
behaves in the face of missing JSON fields.
It was prompted by Arek Olek's comment on
["When Nullability Lies"](https://www.bignerdranch.com/blog/when-nullability-lies-a-cautionary-tale/)
about how Gson's skullduggery in the service of easy JSON parsing
let a not-null field wind up null, go boom under the SQLite lock,
and take out an entire app process.
Arek pointed out that, if you read further, you see that
mochi-kotlin _is_ supposed to address this problem.
Findings:
- Mochi still has the problem.
- But mochi-kotlin's `KotlinJsonAdapter` fixes it!
Thanks, Arek! <3
*/
package jeremywsherman.com.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
private const val TAG = "MAIN_ACTIVITY"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val jsonWithMissingNameString = "{\"number\": 5}"
// loadNameTagFromJsonUsingPlainOldMoshi(jsonWithMissingNameString)
loadNameTagFromJsonUsingMoshiKotlin(jsonWithMissingNameString)
}
/**
This uses a [com.squareup.moshi.JsonAdapter] as demonstrated in the
[intro to the Moshi README](https://github.com/square/moshi/blob/be6f3eb2affcbca1d41a1d396870e052cbbb3bd5/README.md#moshi).
This gives a crash when a method on the null string is called:
```
2019-02-25 10:58:25.707 18147-18147/jeremywsherman.com.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
Process: jeremywsherman.com.myapplication, PID: 18147
java.lang.RuntimeException: Unable to start activity ComponentInfo{jeremywsherman.com.myapplication/jeremywsherman.com.myapplication.MainActivity}: kotlin.TypeCastException: null cannot be cast to non-null type java.lang.String
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2955)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3030)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6938)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Caused by: kotlin.TypeCastException: null cannot be cast to non-null type java.lang.String
at jeremywsherman.com.myapplication.NameTag.toString(MainActivity.kt:30)
at java.lang.String.valueOf(String.java:2827)
at java.lang.StringBuilder.append(StringBuilder.java:132)
at jeremywsherman.com.myapplication.MainActivity.loadNameTagFromJsonUsingPlainOldMoshi(MainActivity.kt:23)
at jeremywsherman.com.myapplication.MainActivity.onCreate(MainActivity.kt:16)
at android.app.Activity.performCreate(Activity.java:7183)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1220)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2908)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3030) 
at android.app.ActivityThread.-wrap11(Unknown Source:0) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696) 
at android.os.Handler.dispatchMessage(Handler.java:105) 
at android.os.Looper.loop(Looper.java:164) 
at android.app.ActivityThread.main(ActivityThread.java:6938) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374) 
```
*/
private fun loadNameTagFromJsonUsingPlainOldMoshi(json: String) {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(NameTag::class.java)
val nameTag = adapter.fromJson(json)
Log.d(TAG, "The shifty-looking elf is wearing a name tag. It says: $nameTag")
}
/**
This uses a [com.squareup.moshi.KotlinJsonAdapterFactory] as demonstrated in the
[Kotlin: Reflection section of the Moshi README](https://github.com/square/moshi/blob/be6f3eb2affcbca1d41a1d396870e052cbbb3bd5/README.md#reflection).
This crashes at runtime as well, but rather than crashing due to a bogus object,
it crashes in fromJson() and correctly diagnoses the missing required field:
```
2019-02-25 11:10:19.611 19190-19190/jeremywsherman.com.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
Process: jeremywsherman.com.myapplication, PID: 19190
java.lang.RuntimeException: Unable to start activity ComponentInfo{jeremywsherman.com.myapplication/jeremywsherman.com.myapplication.MainActivity}: com.squareup.moshi.JsonDataException: Required value 'name' missing at $
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2955)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3030)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6938)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Caused by: com.squareup.moshi.JsonDataException: Required value 'name' missing at $
at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:96)
at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
at com.squareup.moshi.JsonAdapter.fromJson(JsonAdapter.java:41)
at jeremywsherman.com.myapplication.MainActivity.loadNameTagFromJsonUsingMoshiKotlin(MainActivity.kt:78)
at jeremywsherman.com.myapplication.MainActivity.onCreate(MainActivity.kt:19)
at android.app.Activity.performCreate(Activity.java:7183)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1220)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2908)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3030) 
at android.app.ActivityThread.-wrap11(Unknown Source:0) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696) 
at android.os.Handler.dispatchMessage(Handler.java:105) 
at android.os.Looper.loop(Looper.java:164) 
at android.app.ActivityThread.main(ActivityThread.java:6938) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374) 
```
__See also:__ [KotlinJsonAdapterTest#requiredValueAbsent()](https://github.com/square/moshi/blob/master/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt#L134-L144).
*/
private fun loadNameTagFromJsonUsingMoshiKotlin(json: String) {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory()) // <-- the new line
.build()
val adapter = moshi.adapter(NameTag::class.java)
val nameTag = adapter.fromJson(json)
Log.d(TAG, "The shifty-looking elf is wearing a name tag. It says: $nameTag")
}
}
data class NameTag(val name: String, val number: Int) {
override fun toString(): String {
val ordinal = number + 1
val nametagName = name.toUpperCase()
return "HI #$ordinal IS $nametagName"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment