From fe9a303e192625dc710c09191a1fbd31521983b7 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sat, 4 Dec 2021 18:55:11 -0500 Subject: [PATCH] Fail gracefully when a primitive value is absent. Without this we get an AssertionError, which is the wrong exception type for a JSON schema mismatch: java.lang.AssertionError: java.lang.NullPointerException: Cannot invoke "java.lang.Number.intValue()" because the return value of "sun.invoke.util.ValueConversions.primitiveConversion(sun.invoke.util.Wrapper, Object, boolean)" is null at com.squareup.moshi.RecordJsonAdapter.fromJson(RecordJsonAdapter.java:168) at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41) at com.squareup.moshi.JsonAdapter.fromJson(JsonAdapter.java:70) at com.squareup.moshi.records.RecordsTest.absentPrimitiveFails(RecordsTest.java:257) Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Number.intValue()" because the return value of "sun.invoke.util.ValueConversions.primitiveConversion(sun.invoke.util.Wrapper, Object, boolean)" is null at java.base/sun.invoke.util.ValueConversions.unboxInteger(ValueConversions.java:81) at java.base/java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:732) at com.squareup.moshi.RecordJsonAdapter.fromJson(RecordJsonAdapter.java:156) ... 46 more --- .../squareup/moshi/records/RecordsTest.java | 43 +++++++++++++++++++ .../com/squareup/moshi/RecordJsonAdapter.java | 8 ++++ 2 files changed, 51 insertions(+) diff --git a/moshi/records-tests/src/test/java/com/squareup/moshi/records/RecordsTest.java b/moshi/records-tests/src/test/java/com/squareup/moshi/records/RecordsTest.java index f2b33fedd..5c814ac89 100644 --- a/moshi/records-tests/src/test/java/com/squareup/moshi/records/RecordsTest.java +++ b/moshi/records-tests/src/test/java/com/squareup/moshi/records/RecordsTest.java @@ -21,6 +21,7 @@ import com.squareup.moshi.FromJson; import com.squareup.moshi.Json; +import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; @@ -248,4 +249,46 @@ boolean booleanFromJson(JsonReader reader) throws IOException { } public static record BooleanRecord(boolean value) {} + + @Test + public void absentPrimitiveFails() throws IOException { + var adapter = moshi.adapter(AbsentValues.class); + try { + adapter.fromJson("{\"s\":\"\"}"); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Required value 'i' missing at $"); + } + } + + @Test + public void nullPrimitiveFails() throws IOException { + var adapter = moshi.adapter(AbsentValues.class); + try { + adapter.fromJson("{\"s\":\"\",\"i\":null}"); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $.i"); + } + } + + @Test + public void absentObjectIsNull() throws IOException { + var adapter = moshi.adapter(AbsentValues.class); + String json = "{\"i\":5}"; + AbsentValues value = new AbsentValues(null, 5); + assertThat(adapter.fromJson(json)).isEqualTo(value); + assertThat(adapter.toJson(value)).isEqualTo(json); + } + + @Test + public void nullObjectIsNull() throws IOException { + var adapter = moshi.adapter(AbsentValues.class); + String json = "{\"i\":5,\"s\":null}"; + AbsentValues value = new AbsentValues(null, 5); + assertThat(adapter.fromJson(json)).isEqualTo(value); + assertThat(adapter.toJson(value)).isEqualTo("{\"i\":5}"); + } + + public static record AbsentValues(String s, int i) {} } diff --git a/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java b/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java index 5c65ceb90..cc69e40eb 100644 --- a/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java +++ b/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java @@ -157,6 +157,14 @@ public T fromJson(JsonReader reader) throws IOException { } catch (InvocationTargetException e) { throw rethrowCause(e); } catch (Throwable e) { + // Don't throw a fatal error if it's just an absent primitive. + for (int i = 0, limit = componentBindingsArray.length; i < limit; i++) { + if (resultsArray[i] == null + && componentBindingsArray[i].accessor.type().returnType().isPrimitive()) { + throw Util.missingProperty( + componentBindingsArray[i].componentName, componentBindingsArray[i].jsonName, reader); + } + } throw new AssertionError(e); } }