Repository: square/moshi
Issue: Error where map key doesn’t encode as a string is incoherent

When a map key doesn’t have a natural string encoding we crash with a difficult exception. [For example](https://bitbucket.org/robeden/moshi-test), when writing a `Map<Matcher,String> identifiers` we crash like so:

```
Exception in thread "main" java.lang.IllegalStateException: Nesting problem.
        at com.squareup.moshi.JsonUtf8Writer.beforeValue(JsonUtf8Writer.java:367)
        at com.squareup.moshi.JsonUtf8Writer.open(JsonUtf8Writer.java:102)
        at com.squareup.moshi.JsonUtf8Writer.beginObject(JsonUtf8Writer.java:89)
        at com.squareup.moshi.ClassJsonAdapter.toJson(ClassJsonAdapter.java:177)
        at com.squareup.moshi.JsonAdapter$2.toJson(JsonAdapter.java:134)
        at com.squareup.moshi.MapJsonAdapter.toJson(MapJsonAdapter.java:56)
```

Instead we should say “Matcher cannot be used as a map key in JSON.”

The fix for the above example is a JSON adapter, like so:

```
class MatcherJsonAdapter {
  @ToJson String matcherToJson(Matcher m) {
    return m.type + ":" + m.match;
  }

  @FromJson Matcher matcherFromJson(String s) {
    String[] parts = s.split(":", 1);
    return new Matcher(MatchType.valueOf(parts[0]), parts[1]);
  }
}
```

Changed files:
moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java, moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java, moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java, moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java

diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java
index 4fa074d..5cedfe1 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java
@@ -77,6 +77,10 @@ final class JsonUtf8Writer extends JsonWriter {
   }
 
   @Override public JsonWriter beginArray() throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Array cannot be used as a map key in JSON at path " + getPath());
+    }
     writeDeferredName();
     return open(EMPTY_ARRAY, "[");
   }
@@ -86,6 +90,10 @@ final class JsonUtf8Writer extends JsonWriter {
   }
 
   @Override public JsonWriter beginObject() throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Object cannot be used as a map key in JSON at path " + getPath());
+    }
     writeDeferredName();
     return open(EMPTY_OBJECT, "{");
   }
@@ -171,6 +179,10 @@ final class JsonUtf8Writer extends JsonWriter {
   }
 
   @Override public JsonWriter nullValue() throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "null cannot be used as a map key in JSON at path " + getPath());
+    }
     if (deferredName != null) {
       if (serializeNulls) {
         writeDeferredName();
@@ -186,6 +198,10 @@ final class JsonUtf8Writer extends JsonWriter {
   }
 
   @Override public JsonWriter value(boolean value) throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Boolean cannot be used as a map key in JSON at path " + getPath());
+    }
     writeDeferredName();
     beforeValue();
     sink.writeUtf8(value ? "true" : "false");
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java
index 9879739..9bc06b3 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java
@@ -47,6 +47,10 @@ final class JsonValueWriter extends JsonWriter {
   }
 
   @Override public JsonWriter beginArray() throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Array cannot be used as a map key in JSON at path " + getPath());
+    }
     checkStack();
     List<Object> list = new ArrayList<>();
     add(list);
@@ -67,6 +71,10 @@ final class JsonValueWriter extends JsonWriter {
   }
 
   @Override public JsonWriter beginObject() throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Object cannot be used as a map key in JSON at path " + getPath());
+    }
     checkStack();
     Map<String, Object> map = new LinkedHashTreeMap<>();
     add(map);
@@ -116,18 +124,30 @@ final class JsonValueWriter extends JsonWriter {
   }
 
   @Override public JsonWriter nullValue() throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "null cannot be used as a map key in JSON at path " + getPath());
+    }
     add(null);
     pathIndices[stackSize - 1]++;
     return this;
   }
 
   @Override public JsonWriter value(boolean value) throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Boolean cannot be used as a map key in JSON at path " + getPath());
+    }
     add(value);
     pathIndices[stackSize - 1]++;
     return this;
   }
 
   @Override public JsonWriter value(@Nullable Boolean value) throws IOException {
+    if (promoteValueToName) {
+      throw new IllegalStateException(
+          "Boolean cannot be used as a map key in JSON at path " + getPath());
+    }
     add(value);
     pathIndices[stackSize - 1]++;
     return this;
diff --git a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java
index dd3e796..c967047 100644
--- a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java
+++ b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java
@@ -141,6 +141,65 @@ public final class MapJsonAdapterTest {
     assertThat(jsonAdapter.fromJsonValue(jsonObject)).isEqualTo(map);
   }
 
+  @Test public void booleanKeyTypeHasCoherentErrorMessage() {
+    Map<Boolean, String> map = new LinkedHashMap<>();
+    map.put(true, "");
+    JsonAdapter<Map<Boolean, String>> adapter = mapAdapter(Boolean.class, String.class);
+    try {
+      adapter.toJson(map);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessage("Boolean cannot be used as a map key in JSON at path $.");
+    }
+    try {
+      adapter.toJsonValue(map);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessage("Boolean cannot be used as a map key in JSON at path $.");
+    }
+  }
+
+  static final class Key {
+  }
+
+  @Test public void objectKeyTypeHasCoherentErrorMessage() {
+    Map<Key, String> map = new LinkedHashMap<>();
+    map.put(new Key(), "");
+    JsonAdapter<Map<Key, String>> adapter = mapAdapter(Key.class, String.class);
+    try {
+      adapter.toJson(map);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessage("Object cannot be used as a map key in JSON at path $.");
+    }
+    try {
+      adapter.toJsonValue(map);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessage("Object cannot be "
+          + "used as a map key in JSON at path $.");
+    }
+  }
+
+  @Test public void arrayKeyTypeHasCoherentErrorMessage() {
+    Map<String[], String> map = new LinkedHashMap<>();
+    map.put(new String[0], "");
+    JsonAdapter<Map<String[], String>> adapter =
+        mapAdapter(Types.arrayOf(String.class), String.class);
+    try {
+      adapter.toJson(map);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessage("Array cannot be used as a map key in JSON at path $.");
+    }
+    try {
+      adapter.toJsonValue(map);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessage("Array cannot be used as a map key in JSON at path $.");
+    }
+  }
+
   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();
diff --git a/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java b/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java
index c50005d..335e9d0 100644
--- a/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java
+++ b/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java
@@ -252,7 +252,7 @@ public final class PromoteNameToValueTest {
       writer.value(true);
       fail();
     } catch (IllegalStateException e) {
-      assertThat(e).hasMessage("Nesting problem.");
+      assertThat(e).hasMessage("Boolean cannot be used as a map key in JSON at path $.");
     }
     writer.value("true");
     assertThat(writer.getPath()).isEqualTo("$.true");
@@ -283,7 +283,7 @@ public final class PromoteNameToValueTest {
       writer.nullValue();
       fail();
     } catch (IllegalStateException e) {
-      assertThat(e).hasMessage("Nesting problem.");
+      assertThat(e).hasMessage("null cannot be used as a map key in JSON at path $.");
     }
     writer.value("null");
     assertThat(writer.getPath()).isEqualTo("$.null");