From 9e9655b556585a523b20fa5d56fd90fbb0013ac9 Mon Sep 17 00:00:00 2001 From: jwilson Date: Fri, 26 May 2017 21:49:04 -0400 Subject: [PATCH] Change the adapter for Object.class to delegate. Previously if we ever had an opaque Object, the content of this object would always only use the built-in adapters for its members. This changes the built-in Object adapter to do one layer of type checking and then to delegate to user-supplied adapters. The big upside of this is that application code can now change the default numeric type to use when decoding an untyped object. Typically this will be used to replace our default of Double with a user-specified numeric type like BigDecimal. --- .../squareup/moshi/StandardJsonAdapters.java | 35 +++++- .../com/squareup/moshi/ObjectAdapterTest.java | 110 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java index bdb8da120..526dac48e 100644 --- a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java +++ b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java @@ -20,6 +20,7 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Set; @@ -266,13 +267,45 @@ static final class EnumJsonAdapter> extends JsonAdapter { */ static final class ObjectJsonAdapter extends JsonAdapter { private final Moshi moshi; + private final JsonAdapter listJsonAdapter; + private final JsonAdapter mapAdapter; + private final JsonAdapter stringAdapter; + private final JsonAdapter doubleAdapter; + private final JsonAdapter booleanAdapter; ObjectJsonAdapter(Moshi moshi) { this.moshi = moshi; + this.listJsonAdapter = moshi.adapter(List.class); + this.mapAdapter = moshi.adapter(Map.class); + this.stringAdapter = moshi.adapter(String.class); + this.doubleAdapter = moshi.adapter(Double.class); + this.booleanAdapter = moshi.adapter(Boolean.class); } @Override public Object fromJson(JsonReader reader) throws IOException { - return reader.readJsonValue(); + switch (reader.peek()) { + case BEGIN_ARRAY: + return listJsonAdapter.fromJson(reader); + + case BEGIN_OBJECT: + return mapAdapter.fromJson(reader); + + case STRING: + return stringAdapter.fromJson(reader); + + case NUMBER: + return doubleAdapter.fromJson(reader); + + case BOOLEAN: + return booleanAdapter.fromJson(reader); + + case NULL: + return reader.nextNull(); + + default: + throw new IllegalStateException( + "Expected a value but was " + reader.peek() + " at path " + reader.getPath()); + } } @Override public void toJson(JsonWriter writer, Object value) throws IOException { diff --git a/moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java index d85daad7c..a844d9443 100644 --- a/moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java @@ -15,6 +15,10 @@ */ package com.squareup.moshi; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.math.BigDecimal; import java.util.AbstractCollection; import java.util.AbstractList; import java.util.AbstractMap; @@ -26,12 +30,17 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; import org.junit.Ignore; import org.junit.Test; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; public final class ObjectAdapterTest { @Test public void toJsonUsesRuntimeType() throws Exception { @@ -170,6 +179,107 @@ public final class ObjectAdapterTest { assertThat(adapter.toJson(Arrays.asList(new Date(1), new Date(2)))).isEqualTo("[1,2]"); } + /** + * Confirm that the built-in adapter for Object delegates to user-supplied adapters for JSON value + * types like strings. + */ + @Test public void objectAdapterDelegatesStringNamesAndValues() throws Exception { + JsonAdapter stringAdapter = new JsonAdapter() { + @Nullable @Override public String fromJson(JsonReader reader) throws IOException { + return reader.nextString().toUpperCase(Locale.US); + } + + @Override public void toJson(JsonWriter writer, @Nullable String value) { + throw new UnsupportedOperationException(); + } + }; + + Moshi moshi = new Moshi.Builder() + .add(String.class, stringAdapter) + .build(); + JsonAdapter objectAdapter = moshi.adapter(Object.class); + Map value = (Map) objectAdapter.fromJson("{\"a\":\"b\", \"c\":\"d\"}"); + assertThat(value).containsExactly(entry("A", "B"), entry("C", "D")); + } + + /** + * Confirm that the built-in adapter for Object delegates to any user-supplied adapters for + * Object. This is necessary to customize adapters for primitives like numbers. + */ + @Test public void objectAdapterDelegatesObjects() throws Exception { + JsonAdapter.Factory objectFactory = new JsonAdapter.Factory() { + @Override public @Nullable JsonAdapter create( + Type type, Set annotations, Moshi moshi) { + if (type != Object.class) return null; + + final JsonAdapter delegate = moshi.nextAdapter(this, Object.class, annotations); + return new JsonAdapter() { + @Override public @Nullable Object fromJson(JsonReader reader) throws IOException { + if (reader.peek() != JsonReader.Token.NUMBER) { + return delegate.fromJson(reader); + } else { + return new BigDecimal(reader.nextString()); + } + } + + @Override public void toJson(JsonWriter writer, @Nullable Object value) { + throw new UnsupportedOperationException(); + } + }; + } + }; + + Moshi moshi = new Moshi.Builder() + .add(objectFactory) + .build(); + JsonAdapter objectAdapter = moshi.adapter(Object.class); + List value = (List) objectAdapter.fromJson("[0, 1, 2.0, 3.14]"); + assertThat(value).isEqualTo(Arrays.asList(new BigDecimal("0"), new BigDecimal("1"), + new BigDecimal("2.0"), new BigDecimal("3.14"))); + } + + /** Confirm that the built-in adapter for Object delegates to user-supplied adapters for lists. */ + @Test public void objectAdapterDelegatesLists() throws Exception { + JsonAdapter> listAdapter = new JsonAdapter>() { + @Override public @Nullable List fromJson(JsonReader reader) throws IOException { + reader.skipValue(); + return singletonList("z"); + } + + @Override public void toJson(JsonWriter writer, @Nullable List value) { + throw new UnsupportedOperationException(); + } + }; + + Moshi moshi = new Moshi.Builder() + .add(List.class, listAdapter) + .build(); + JsonAdapter objectAdapter = moshi.adapter(Object.class); + Map mapOfList = (Map) objectAdapter.fromJson("{\"a\":[\"b\"]}"); + assertThat(mapOfList).isEqualTo(singletonMap("a", singletonList("z"))); + } + + /** Confirm that the built-in adapter for Object delegates to user-supplied adapters for maps. */ + @Test public void objectAdapterDelegatesMaps() throws Exception { + JsonAdapter> mapAdapter = new JsonAdapter>() { + @Override public @Nullable Map fromJson(JsonReader reader) throws IOException { + reader.skipValue(); + return singletonMap("x", "y"); + } + + @Override public void toJson(JsonWriter writer, @Nullable Map value) { + throw new UnsupportedOperationException(); + } + }; + + Moshi moshi = new Moshi.Builder() + .add(Map.class, mapAdapter) + .build(); + JsonAdapter objectAdapter = moshi.adapter(Object.class); + List listOfMap = (List) objectAdapter.fromJson("[{\"b\":\"c\"}]"); + assertThat(listOfMap).isEqualTo(singletonList(singletonMap("x", "y"))); + } + static class Delivery { String address; List items;