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.
This commit is contained in:
jwilson
2017-05-26 21:49:04 -04:00
parent 798f14bda5
commit 9e9655b556
2 changed files with 144 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -266,13 +267,45 @@ final class StandardJsonAdapters {
*/ */
static final class ObjectJsonAdapter extends JsonAdapter<Object> { static final class ObjectJsonAdapter extends JsonAdapter<Object> {
private final Moshi moshi; private final Moshi moshi;
private final JsonAdapter<List> listJsonAdapter;
private final JsonAdapter<Map> mapAdapter;
private final JsonAdapter<String> stringAdapter;
private final JsonAdapter<Double> doubleAdapter;
private final JsonAdapter<Boolean> booleanAdapter;
ObjectJsonAdapter(Moshi moshi) { ObjectJsonAdapter(Moshi moshi) {
this.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 { @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 { @Override public void toJson(JsonWriter writer, Object value) throws IOException {

View File

@@ -15,6 +15,10 @@
*/ */
package com.squareup.moshi; 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.AbstractCollection;
import java.util.AbstractList; import java.util.AbstractList;
import java.util.AbstractMap; import java.util.AbstractMap;
@@ -26,12 +30,17 @@ import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; 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.assertThat;
import static org.assertj.core.api.Assertions.entry;
public final class ObjectAdapterTest { public final class ObjectAdapterTest {
@Test public void toJsonUsesRuntimeType() throws Exception { @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]"); 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<String> stringAdapter = new JsonAdapter<String>() {
@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<Object> 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<? extends Annotation> annotations, Moshi moshi) {
if (type != Object.class) return null;
final JsonAdapter<Object> delegate = moshi.nextAdapter(this, Object.class, annotations);
return new JsonAdapter<Object>() {
@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<Object> 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<List<?>> listAdapter = new JsonAdapter<List<?>>() {
@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<Object> 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<Map<?, ?>> mapAdapter = new JsonAdapter<Map<?, ?>>() {
@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<Object> objectAdapter = moshi.adapter(Object.class);
List<?> listOfMap = (List<?>) objectAdapter.fromJson("[{\"b\":\"c\"}]");
assertThat(listOfMap).isEqualTo(singletonList(singletonMap("x", "y")));
}
static class Delivery { static class Delivery {
String address; String address;
List<Object> items; List<Object> items;