Last active
February 25, 2019 16:28
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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