diff --git a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java index 437d956..aadc866 100644 --- a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java +++ b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java @@ -50,6 +50,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { @Override public void toJson(JsonWriter writer, Object value) throws IOException { if (toAdapter == null) { delegate.toJson(writer, value); + } else if (!toAdapter.nullable && value == null) { + writer.nullValue(); } else { try { toAdapter.toJson(moshi, writer, value); @@ -65,6 +67,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { @Override public Object fromJson(JsonReader reader) throws IOException { if (fromAdapter == null) { return delegate.fromJson(reader); + } else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) { + reader.nextNull(); + return null; } else { try { return fromAdapter.fromJson(moshi, reader); @@ -132,7 +137,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { // public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception { Set parameterAnnotations = Util.jsonAnnotations(method.getParameterAnnotations()[1]); - return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method) { + return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method, false) { @Override public void toJson(Moshi moshi, JsonWriter writer, Object value) throws IOException, InvocationTargetException, IllegalAccessException { method.invoke(adapter, writer, value); @@ -142,9 +147,11 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { } else if (parameterTypes.length == 1 && returnType != void.class) { // public List pointToJson(Point point) throws Exception { final Set returnTypeAnnotations = Util.jsonAnnotations(method); - Set parameterAnnotations = - Util.jsonAnnotations(method.getParameterAnnotations()[0]); - return new AdapterMethod(parameterTypes[0], parameterAnnotations, adapter, method) { + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + Set qualifierAnnotations = + Util.jsonAnnotations(parameterAnnotations[0]); + boolean nullable = Util.hasNullable(parameterAnnotations[0]); + return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method, nullable) { @Override public void toJson(Moshi moshi, JsonWriter writer, Object value) throws IOException, InvocationTargetException, IllegalAccessException { JsonAdapter delegate = moshi.adapter(returnType, returnTypeAnnotations); @@ -175,7 +182,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { && returnType != void.class) { // public Point pointFromJson(JsonReader jsonReader) throws Exception { Set returnTypeAnnotations = Util.jsonAnnotations(method); - return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method) { + return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, false) { @Override public Object fromJson(Moshi moshi, JsonReader reader) throws IOException, IllegalAccessException, InvocationTargetException { return method.invoke(adapter, reader); @@ -185,12 +192,14 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { } else if (parameterTypes.length == 1 && returnType != void.class) { // public Point pointFromJson(List o) throws Exception { Set returnTypeAnnotations = Util.jsonAnnotations(method); - final Set parameterAnnotations - = Util.jsonAnnotations(method.getParameterAnnotations()[0]); - return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method) { + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + final Set qualifierAnnotations + = Util.jsonAnnotations(parameterAnnotations[0]); + boolean nullable = Util.hasNullable(parameterAnnotations[0]); + return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, nullable) { @Override public Object fromJson(Moshi moshi, JsonReader reader) throws IOException, IllegalAccessException, InvocationTargetException { - JsonAdapter delegate = moshi.adapter(parameterTypes[0], parameterAnnotations); + JsonAdapter delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations); Object intermediate = delegate.fromJson(reader); return method.invoke(adapter, intermediate); } @@ -221,13 +230,15 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { final Set annotations; final Object adapter; final Method method; + final boolean nullable; public AdapterMethod(Type type, - Set annotations, Object adapter, Method method) { + Set annotations, Object adapter, Method method, boolean nullable) { this.type = type; this.annotations = annotations; this.adapter = adapter; this.method = method; + this.nullable = nullable; } public void toJson(Moshi moshi, JsonWriter writer, Object value) diff --git a/moshi/src/main/java/com/squareup/moshi/Util.java b/moshi/src/main/java/com/squareup/moshi/Util.java index 114bb49..eddb6b6 100644 --- a/moshi/src/main/java/com/squareup/moshi/Util.java +++ b/moshi/src/main/java/com/squareup/moshi/Util.java @@ -53,4 +53,14 @@ final class Util { } return false; } + + /** Returns true if {@code annotations} has any annotation whose simple name is Nullable. */ + public static boolean hasNullable(Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation.annotationType().getSimpleName().equals("Nullable")) { + return true; + } + } + return false; + } } diff --git a/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java b/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java index c6fb984..f864f57 100644 --- a/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java +++ b/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java @@ -16,6 +16,8 @@ package com.squareup.moshi; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -181,6 +183,56 @@ public final class AdapterMethodsTest { } } + /** + * Simple adapter methods are not invoked for null values unless they're annotated {@code + * @Nullable}. (The specific annotation class doesn't matter; just its simple name.) + */ + @Test public void toAndFromNullNotNullable() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new NotNullablePointAsListOfIntegersJsonAdapter()) + .build(); + JsonAdapter pointAdapter = moshi.adapter(Point.class).lenient(); + assertThat(pointAdapter.toJson(null)).isEqualTo("null"); + assertThat(pointAdapter.fromJson("null")).isNull(); + } + + static class NotNullablePointAsListOfIntegersJsonAdapter { + @ToJson List pointToJson(Point point) { + throw new AssertionError(); + } + + @FromJson Point pointFromJson(List o) throws Exception { + throw new AssertionError(); + } + } + + @Test public void toAndFromNullNullable() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new NullablePointAsListOfIntegersJsonAdapter()) + .build(); + JsonAdapter pointAdapter = moshi.adapter(Point.class).lenient(); + assertThat(pointAdapter.toJson(null)).isEqualTo("[0,0]"); + assertThat(pointAdapter.fromJson("null")).isEqualTo(new Point(0, 0)); + } + + static class NullablePointAsListOfIntegersJsonAdapter { + @ToJson List pointToJson(@Nullable Point point) { + return point != null + ? Arrays.asList(point.x, point.y) + : Arrays.asList(0, 0); + } + + @FromJson Point pointFromJson(@Nullable List o) throws Exception { + if (o == null) return new Point(0, 0); + if (o.size() == 2) return new Point(o.get(0), o.get(1)); + throw new Exception("Expected null or 2 elements but was " + o); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Nullable { + } + static class Point { final int x; final int y;