mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Merge pull request #20 from square/jwilson_0323_map_adapter
Map adapter.
This commit is contained in:
@@ -46,7 +46,7 @@ class ArrayJsonAdapter extends JsonAdapter<Object> {
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
List<Object> list = new ArrayList<Object>();
|
||||
List<Object> list = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
list.add(elementAdapter.fromJson(reader));
|
||||
|
@@ -46,7 +46,7 @@ abstract class ClassFactory<T> {
|
||||
return (T) constructor.newInstance(args);
|
||||
}
|
||||
};
|
||||
} catch (NoSuchMethodException noNoArgsConstructor) {
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// No no-args constructor. Fall back to something more magical...
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ abstract class ClassFactory<T> {
|
||||
};
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException notJvm) {
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException ignored) {
|
||||
// Not the expected version of the Oracle Java library!
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ abstract class ClassFactory<T> {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchMethodException notLibcore) {
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// Not the expected version of Dalvik/libcore!
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,7 @@ import java.util.TreeMap;
|
||||
* of classes in {@code java.*}, {@code javax.*} and {@code android.*} are omitted from both
|
||||
* serialization and deserialization unless they are either public or protected.
|
||||
*/
|
||||
final class ClassAdapter<T> extends JsonAdapter<T> {
|
||||
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
@@ -54,7 +54,7 @@ final class ClassAdapter<T> extends JsonAdapter<T> {
|
||||
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
|
||||
createFieldBindings(moshi, t, fields);
|
||||
}
|
||||
return new ClassAdapter<>(classFactory, fields).nullSafe();
|
||||
return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
|
||||
}
|
||||
|
||||
/** Creates a field binding for each of declared field of {@code type}. */
|
||||
@@ -103,7 +103,7 @@ final class ClassAdapter<T> extends JsonAdapter<T> {
|
||||
private final ClassFactory<T> classFactory;
|
||||
private final Map<String, FieldBinding<?>> jsonFields;
|
||||
|
||||
private ClassAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
|
||||
private ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
|
||||
this.classFactory = classFactory;
|
||||
this.jsonFields = jsonFields;
|
||||
}
|
@@ -49,7 +49,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
|
||||
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
||||
return new CollectionJsonAdapter<Collection<T>, T>(elementAdapter) {
|
||||
@Override Collection<T> newCollection() {
|
||||
return new ArrayList<T>();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -59,7 +59,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
|
||||
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
||||
return new CollectionJsonAdapter<Set<T>, T>(elementAdapter) {
|
||||
@Override Set<T> newCollection() {
|
||||
return new LinkedHashSet<T>();
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
69
moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java
Normal file
69
moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Converts maps with string keys to JSON objects.
|
||||
*
|
||||
* TODO: support maps with other key types and convert to/from strings.
|
||||
*/
|
||||
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||
public static final Factory FACTORY = new Factory() {
|
||||
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (rawType != Map.class) return null;
|
||||
Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType);
|
||||
if (keyAndValue[0] != String.class) return null;
|
||||
return new MapJsonAdapter<>(moshi, keyAndValue[1]).nullSafe();
|
||||
};
|
||||
};
|
||||
|
||||
private final JsonAdapter<V> valueAdapter;
|
||||
|
||||
public MapJsonAdapter(Moshi moshi, Type valueType) {
|
||||
this.valueAdapter = moshi.adapter(valueType);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Map<K, V> map) throws IOException {
|
||||
writer.beginObject();
|
||||
for (Map.Entry<K, V> entry : map.entrySet()) {
|
||||
writer.name((String) entry.getKey());
|
||||
valueAdapter.toJson(writer, entry.getValue());
|
||||
}
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
@Override public Map<K, V> fromJson(JsonReader reader) throws IOException {
|
||||
LinkedHashTreeMap<K, V> result = new LinkedHashTreeMap<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
@SuppressWarnings("unchecked") // Currently 'K' is always 'String'.
|
||||
K name = (K) reader.nextName();
|
||||
V value = valueAdapter.fromJson(reader);
|
||||
V replaced = result.put(name, value);
|
||||
if (replaced != null) {
|
||||
throw new IllegalArgumentException("object property '" + name + "' has multiple values");
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -28,7 +28,7 @@ import static com.squareup.moshi.Util.NO_ANNOTATIONS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public final class ClassAdapterTest {
|
||||
public final class ClassJsonAdapterTest {
|
||||
private final Moshi moshi = new Moshi.Builder().build();
|
||||
|
||||
static class BasicPizza {
|
||||
@@ -156,12 +156,12 @@ public final class ClassAdapterTest {
|
||||
|
||||
@Test public void fieldNameCollision() throws Exception {
|
||||
try {
|
||||
ClassAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi);
|
||||
ClassJsonAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("field name collision: 'a' declared by both "
|
||||
+ "com.squareup.moshi.ClassAdapterTest$ExtendsBaseA and "
|
||||
+ "superclass com.squareup.moshi.ClassAdapterTest$BaseA");
|
||||
+ "com.squareup.moshi.ClassJsonAdapterTest$ExtendsBaseA and "
|
||||
+ "superclass com.squareup.moshi.ClassJsonAdapterTest$BaseA");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,17 +302,17 @@ public final class ClassAdapterTest {
|
||||
|
||||
@Test public void nonStaticNestedClassNotSupported() throws Exception {
|
||||
try {
|
||||
ClassAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi);
|
||||
ClassJsonAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("cannot serialize non-static nested class "
|
||||
+ "com.squareup.moshi.ClassAdapterTest$NonStatic");
|
||||
+ "com.squareup.moshi.ClassJsonAdapterTest$NonStatic");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void platformClassNotSupported() throws Exception {
|
||||
assertThat(ClassAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
assertThat(ClassAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
}
|
||||
|
||||
@Test public void anonymousClassNotSupported() throws Exception {
|
||||
@@ -322,7 +322,7 @@ public final class ClassAdapterTest {
|
||||
}
|
||||
};
|
||||
try {
|
||||
ClassAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi);
|
||||
ClassJsonAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("cannot serialize anonymous class " + c.getClass().getName());
|
||||
@@ -330,7 +330,7 @@ public final class ClassAdapterTest {
|
||||
}
|
||||
|
||||
@Test public void interfaceNotSupported() throws Exception {
|
||||
assertThat(ClassAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
}
|
||||
|
||||
static abstract class Abstract {
|
||||
@@ -338,11 +338,11 @@ public final class ClassAdapterTest {
|
||||
|
||||
@Test public void abstractClassNotSupported() throws Exception {
|
||||
try {
|
||||
ClassAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi);
|
||||
ClassJsonAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("cannot serialize abstract class "
|
||||
+ "com.squareup.moshi.ClassAdapterTest$Abstract");
|
||||
+ "com.squareup.moshi.ClassJsonAdapterTest$Abstract");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ public final class ClassAdapterTest {
|
||||
|
||||
private <T> String toJson(Class<T> type, T value) throws IOException {
|
||||
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
|
||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassAdapter.FACTORY.create(
|
||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
|
||||
type, NO_ANNOTATIONS, moshi);
|
||||
|
||||
// Wrap in an array to avoid top-level object warnings without going completely lenient.
|
||||
@@ -408,7 +408,7 @@ public final class ClassAdapterTest {
|
||||
|
||||
private <T> T fromJson(Class<T> type, String json) throws IOException {
|
||||
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
|
||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassAdapter.FACTORY.create(
|
||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
|
||||
type, NO_ANNOTATIONS, moshi);
|
||||
// Wrap in an array to avoid top-level object warnings without going completely lenient.
|
||||
JsonReader jsonReader = new JsonReader("[" + json + "]");
|
129
moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java
Normal file
129
moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import okio.Buffer;
|
||||
import org.assertj.core.data.MapEntry;
|
||||
import org.junit.Test;
|
||||
|
||||
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public final class MapJsonAdapterTest {
|
||||
private final Moshi moshi = new Moshi.Builder().build();
|
||||
|
||||
@Test public void map() throws Exception {
|
||||
Map<String, Boolean> map = new LinkedHashMap<>();
|
||||
map.put("a", true);
|
||||
map.put("b", false);
|
||||
map.put("c", null);
|
||||
|
||||
String toJson = toJson(String.class, Boolean.class, map);
|
||||
assertThat(toJson).isEqualTo("{\"a\":true,\"b\":false,\"c\":null}");
|
||||
|
||||
Map<String, Boolean> fromJson = fromJson(
|
||||
String.class, Boolean.class, "{\"a\":true,\"b\":false,\"c\":null}");
|
||||
assertThat(fromJson).containsExactly(
|
||||
MapEntry.entry("a", true), MapEntry.entry("b", false), MapEntry.entry("c", null));
|
||||
}
|
||||
|
||||
@Test public void mapWithNullKeyFailsToEmit() throws Exception {
|
||||
Map<String, Boolean> map = new LinkedHashMap<>();
|
||||
map.put(null, true);
|
||||
|
||||
try {
|
||||
toJson(String.class, Boolean.class, map);
|
||||
fail();
|
||||
} catch (NullPointerException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void emptyMap() throws Exception {
|
||||
Map<String, Boolean> map = new LinkedHashMap<>();
|
||||
|
||||
String toJson = toJson(String.class, Boolean.class, map);
|
||||
assertThat(toJson).isEqualTo("{}");
|
||||
|
||||
Map<String, Boolean> fromJson = fromJson(String.class, Boolean.class, "{}");
|
||||
assertThat(fromJson).isEmpty();
|
||||
}
|
||||
|
||||
@Test public void nullMap() throws Exception {
|
||||
JsonAdapter<?> jsonAdapter = mapAdapter(String.class, Boolean.class);
|
||||
|
||||
Buffer buffer = new Buffer();
|
||||
JsonWriter jsonWriter = new JsonWriter(buffer);
|
||||
jsonWriter.setLenient(true);
|
||||
jsonAdapter.toJson(jsonWriter, null);
|
||||
assertThat(buffer.readUtf8()).isEqualTo("null");
|
||||
|
||||
JsonReader jsonReader = new JsonReader("null");
|
||||
jsonReader.setLenient(true);
|
||||
assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null);
|
||||
}
|
||||
|
||||
@Test public void orderIsRetained() throws Exception {
|
||||
Map<String, Integer> map = new LinkedHashMap<>();
|
||||
map.put("c", 1);
|
||||
map.put("a", 2);
|
||||
map.put("d", 3);
|
||||
map.put("b", 4);
|
||||
|
||||
String toJson = toJson(String.class, Integer.class, map);
|
||||
assertThat(toJson).isEqualTo("{\"c\":1,\"a\":2,\"d\":3,\"b\":4}");
|
||||
|
||||
Map<String, Integer> fromJson = fromJson(
|
||||
String.class, Integer.class, "{\"c\":1,\"a\":2,\"d\":3,\"b\":4}");
|
||||
assertThat(new ArrayList<Object>(fromJson.keySet()))
|
||||
.isEqualTo(Arrays.asList("c", "a", "d", "b"));
|
||||
}
|
||||
|
||||
@Test public void duplicatesAreForbidden() throws Exception {
|
||||
try {
|
||||
fromJson(String.class, Integer.class, "{\"c\":1,\"c\":2}");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("object property 'c' has multiple values");
|
||||
}
|
||||
}
|
||||
|
||||
private <K, V> String toJson(Type keyType, Type valueType, Map<K, V> value) throws IOException {
|
||||
JsonAdapter<Map<K, V>> jsonAdapter = mapAdapter(keyType, valueType);
|
||||
Buffer buffer = new Buffer();
|
||||
JsonWriter jsonWriter = new JsonWriter(buffer);
|
||||
jsonWriter.setSerializeNulls(true);
|
||||
jsonAdapter.toJson(jsonWriter, value);
|
||||
return buffer.readUtf8();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // It's the caller's responsibility to make sure K and V match.
|
||||
private <K, V> JsonAdapter<Map<K, V>> mapAdapter(Type keyType, Type valueType) {
|
||||
return (JsonAdapter<Map<K, V>>) MapJsonAdapter.FACTORY.create(
|
||||
Types.newParameterizedType(Map.class, keyType, valueType), NO_ANNOTATIONS, moshi);
|
||||
}
|
||||
|
||||
private <K, V> Map<K, V> fromJson(Type keyType, Type valueType, String json) throws IOException {
|
||||
JsonAdapter<Map<K, V>> mapJsonAdapter = mapAdapter(keyType, valueType);
|
||||
return mapJsonAdapter.fromJson(json);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user