diff --git a/moshi/japicmp/build.gradle.kts b/moshi/japicmp/build.gradle.kts index 213eff6..36ae1cd 100644 --- a/moshi/japicmp/build.gradle.kts +++ b/moshi/japicmp/build.gradle.kts @@ -35,7 +35,8 @@ val japicmp = tasks.register("japicmp") { "com.squareup.moshi.JsonAdapter#indent(java.lang.String)" // Was unintentionally open before ) fieldExcludes = listOf( - "com.squareup.moshi.CollectionJsonAdapter#FACTORY" // False-positive, class is not public anyway + "com.squareup.moshi.CollectionJsonAdapter#FACTORY", // False-positive, class is not public anyway + "com.squareup.moshi.MapJsonAdapter#FACTORY" // Class is not public ) } diff --git a/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java b/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java index ffce2d4..8a8903b 100644 --- a/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java +++ b/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java @@ -26,6 +26,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.NoSuchElementException; import java.util.Set; +import javax.annotation.Nullable; /** * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses insertion order for @@ -90,7 +91,7 @@ final class LinkedHashTreeMap extends AbstractMap implements Seriali } @Override - public V put(K key, V value) { + public V put(K key, @Nullable V value) { if (key == null) { throw new NullPointerException("key == null"); } diff --git a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java deleted file mode 100644 index 0e0d83a..0000000 --- a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 - * - * https://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.annotation.Annotation; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nullable; - -/** - * Converts maps with string keys to JSON objects. - * - *

TODO: support maps with other key types and convert to/from strings. - */ -final class MapJsonAdapter extends JsonAdapter> { - public static final Factory FACTORY = - new Factory() { - @Override - public @Nullable JsonAdapter create( - Type type, Set annotations, Moshi moshi) { - if (!annotations.isEmpty()) return null; - Class rawType = Types.getRawType(type); - if (rawType != Map.class) return null; - Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType); - return new MapJsonAdapter<>(moshi, keyAndValue[0], keyAndValue[1]).nullSafe(); - } - }; - - private final JsonAdapter keyAdapter; - private final JsonAdapter valueAdapter; - - MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) { - this.keyAdapter = moshi.adapter(keyType); - this.valueAdapter = moshi.adapter(valueType); - } - - @Override - public void toJson(JsonWriter writer, Map map) throws IOException { - writer.beginObject(); - for (Map.Entry entry : map.entrySet()) { - if (entry.getKey() == null) { - throw new JsonDataException("Map key is null at " + writer.getPath()); - } - writer.promoteValueToName(); - keyAdapter.toJson(writer, entry.getKey()); - valueAdapter.toJson(writer, entry.getValue()); - } - writer.endObject(); - } - - @Override - public Map fromJson(JsonReader reader) throws IOException { - LinkedHashTreeMap result = new LinkedHashTreeMap<>(); - reader.beginObject(); - while (reader.hasNext()) { - reader.promoteNameToValue(); - K name = keyAdapter.fromJson(reader); - V value = valueAdapter.fromJson(reader); - V replaced = result.put(name, value); - if (replaced != null) { - throw new JsonDataException( - "Map key '" - + name - + "' has multiple values at path " - + reader.getPath() - + ": " - + replaced - + " and " - + value); - } - } - reader.endObject(); - return result; - } - - @Override - public String toString() { - return "JsonAdapter(" + keyAdapter + "=" + valueAdapter + ")"; - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt new file mode 100644 index 0000000..432f70f --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt @@ -0,0 +1,73 @@ +/* + * 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 + * + * https://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 com.squareup.moshi.internal.knownNotNull +import java.lang.reflect.Type + +/** + * Converts maps with string keys to JSON objects. + * + * TODO: support maps with other key types and convert to/from strings. + */ +internal class MapJsonAdapter(moshi: Moshi, keyType: Type, valueType: Type) : JsonAdapter>() { + private val keyAdapter: JsonAdapter = moshi.adapter(keyType) + private val valueAdapter: JsonAdapter = moshi.adapter(valueType) + + override fun toJson(writer: JsonWriter, map: Map?) { + writer.beginObject() + // Never null because we wrap in nullSafe() + for ((key, value) in knownNotNull(map)) { + if (key == null) { + throw JsonDataException("Map key is null at " + writer.path) + } + writer.promoteValueToName() + keyAdapter.toJson(writer, key) + valueAdapter.toJson(writer, value) + } + writer.endObject() + } + + override fun fromJson(reader: JsonReader): Map { + val result = LinkedHashTreeMap() + reader.beginObject() + while (reader.hasNext()) { + reader.promoteNameToValue() + val name = keyAdapter.fromJson(reader) ?: throw JsonDataException("Map key is null at ${reader.path}") + val value = valueAdapter.fromJson(reader) + val replaced = result.put(name, value) + if (replaced != null) { + throw JsonDataException( + "Map key '$name' has multiple values at path ${reader.path}: $replaced and $value" + ) + } + } + reader.endObject() + return result + } + + override fun toString() = "JsonAdapter($keyAdapter=$valueAdapter)" + + companion object Factory : JsonAdapter.Factory { + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + if (annotations.isNotEmpty()) return null + val rawType = type.rawType + if (rawType != Map::class.java) return null + val keyAndValue = Types.mapKeyAndValueTypes(type, rawType) + return MapJsonAdapter(moshi, keyAndValue[0], keyAndValue[1]).nullSafe() + } + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.kt b/moshi/src/main/java/com/squareup/moshi/Moshi.kt index ba3af7f..2f2a012 100644 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.kt +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.kt @@ -366,7 +366,7 @@ public class Moshi internal constructor(builder: Builder) { val BUILT_IN_FACTORIES: List = buildList(6) { add(StandardJsonAdapters) add(CollectionJsonAdapter.Factory) - add(MapJsonAdapter.FACTORY) + add(MapJsonAdapter.Factory) add(ArrayJsonAdapter.FACTORY) add(RecordJsonAdapter.FACTORY) add(ClassJsonAdapter.FACTORY) diff --git a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java index 3e54894..740d606 100644 --- a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java @@ -262,7 +262,7 @@ public final class MapJsonAdapterTest { @SuppressWarnings("unchecked") // It's the caller's responsibility to make sure K and V match. private JsonAdapter> mapAdapter(Type keyType, Type valueType) { return (JsonAdapter>) - MapJsonAdapter.FACTORY.create( + MapJsonAdapter.Factory.create( Types.newParameterizedType(Map.class, keyType, valueType), NO_ANNOTATIONS, moshi); }