Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link to troubleshooting guide from exception messages #2357

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 93 additions & 22 deletions Troubleshooting.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gson/src/main/java/com/google/gson/Gson.java
Expand Up @@ -105,7 +105,7 @@
* <p>See the <a href="https://github.com/google/gson/blob/master/UserGuide.md">Gson User Guide</a>
* for a more complete set of examples.</p>
*
* <h2>Lenient JSON handling</h2>
* <h2 id="default-lenient">Lenient JSON handling</h2>
* For legacy reasons most of the {@code Gson} methods allow JSON data which does not
* comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()}
* is used or not. If this behavior is not desired, the following workarounds can be used:
Expand Down
@@ -0,0 +1,12 @@
package com.google.gson.internal;

public class TroubleshootingGuide {
private TroubleshootingGuide() {}

/**
* Creates a URL referring to the specified troubleshooting section.
*/
public static String createUrl(String id) {
return "https://github.com/google/gson/blob/master/Troubleshooting.md#" + id;
}
}
Expand Up @@ -33,6 +33,7 @@
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Primitives;
import com.google.gson.internal.ReflectionAccessFilterHelper;
import com.google.gson.internal.TroubleshootingGuide;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
Expand Down Expand Up @@ -114,7 +115,7 @@ public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException(
"ReflectionAccessFilter does not permit using reflection for " + raw
+ ". Register a TypeAdapter for this type or adjust the access filter.");
+ ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;

Expand Down Expand Up @@ -306,7 +307,8 @@ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type,
if (previous != null) {
throw new IllegalArgumentException("Class " + originalRaw.getName()
+ " declares multiple JSON fields named '" + previous.name + "'; conflict is caused"
+ " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field));
+ " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field)
+ "\nSee " + TroubleshootingGuide.createUrl("duplicate-fields"));
}
}
type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
Expand Down
Expand Up @@ -28,6 +28,7 @@
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.SerializedName;
import com.google.gson.internal.LazilyParsedNumber;
import com.google.gson.internal.TroubleshootingGuide;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
Expand Down Expand Up @@ -73,12 +74,14 @@ private TypeAdapters() {
@Override
public void write(JsonWriter out, Class value) throws IOException {
throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: "
+ value.getName() + ". Forgot to register a type adapter?");
+ value.getName() + ". Forgot to register a type adapter?"
+ "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported"));
}
@Override
public Class read(JsonReader in) throws IOException {
throw new UnsupportedOperationException(
"Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?");
"Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"
+ "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported"));
}
}.nullSafe();

Expand Down
Expand Up @@ -18,6 +18,7 @@

import com.google.gson.JsonIOException;
import com.google.gson.internal.GsonBuildConfig;
import com.google.gson.internal.TroubleshootingGuide;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
Expand All @@ -40,6 +41,17 @@ public class ReflectionHelper {

private ReflectionHelper() {}

private static String getInaccessibleTroubleshootingSuffix(Exception e) {
// Class was added in Java 9, therefore cannot use instanceof
if (e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
String message = e.getMessage();
String troubleshootingId = message != null && message.contains("to module com.google.gson")
? "reflection-inaccessible-to-module-gson" : "reflection-inaccessible";
return "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId);
}
return "";
}

/**
* Internal implementation of making an {@link AccessibleObject} accessible.
*
Expand All @@ -52,7 +64,8 @@ public static void makeAccessible(AccessibleObject object) throws JsonIOExceptio
} catch (Exception exception) {
String description = getAccessibleObjectDescription(object, false);
throw new JsonIOException("Failed making " + description + " accessible; either increase its visibility"
+ " or write a custom TypeAdapter for its declaring type.", exception);
+ " or write a custom TypeAdapter for its declaring type." + getInaccessibleTroubleshootingSuffix(exception),
exception);
}
}

Expand Down Expand Up @@ -142,7 +155,7 @@ public static String tryMakeAccessible(Constructor<?> constructor) {
return "Failed making constructor '" + constructorToString(constructor) + "' accessible;"
+ " either increase its visibility or write a custom InstanceCreator or TypeAdapter for"
// Include the message since it might contain more detailed information
+ " its declaring type: " + exception.getMessage();
+ " its declaring type: " + exception.getMessage() + getInaccessibleTroubleshootingSuffix(exception);
}
}

Expand Down
46 changes: 27 additions & 19 deletions gson/src/main/java/com/google/gson/stream/JsonReader.java
Expand Up @@ -17,6 +17,7 @@
package com.google.gson.stream;

import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.internal.TroubleshootingGuide;
import com.google.gson.internal.bind.JsonTreeReader;
import java.io.Closeable;
import java.io.EOFException;
Expand Down Expand Up @@ -355,7 +356,7 @@ public void beginArray() throws IOException {
pathIndices[stackSize - 1] = 0;
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + locationString());
throw unexpectedTokenError("BEGIN_ARRAY");
}
}

Expand All @@ -373,7 +374,7 @@ public void endArray() throws IOException {
pathIndices[stackSize - 1]++;
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected END_ARRAY but was " + peek() + locationString());
throw unexpectedTokenError("END_ARRAY");
}
}

Expand All @@ -390,7 +391,7 @@ public void beginObject() throws IOException {
push(JsonScope.EMPTY_OBJECT);
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() + locationString());
throw unexpectedTokenError("BEGIN_OBJECT");
}
}

Expand All @@ -409,7 +410,7 @@ public void endObject() throws IOException {
pathIndices[stackSize - 1]++;
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected END_OBJECT but was " + peek() + locationString());
throw unexpectedTokenError("END_OBJECT");
}
}

Expand Down Expand Up @@ -797,7 +798,7 @@ public String nextName() throws IOException {
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
result = nextQuotedValue('"');
} else {
throw new IllegalStateException("Expected a name but was " + peek() + locationString());
throw unexpectedTokenError("a name");
}
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = result;
Expand Down Expand Up @@ -833,7 +834,7 @@ public String nextString() throws IOException {
result = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
} else {
throw new IllegalStateException("Expected a string but was " + peek() + locationString());
throw unexpectedTokenError("a string");
}
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
Expand Down Expand Up @@ -861,7 +862,7 @@ public boolean nextBoolean() throws IOException {
pathIndices[stackSize - 1]++;
return false;
}
throw new IllegalStateException("Expected a boolean but was " + peek() + locationString());
throw unexpectedTokenError("a boolean");
}

/**
Expand All @@ -880,7 +881,7 @@ public void nextNull() throws IOException {
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
} else {
throw new IllegalStateException("Expected null but was " + peek() + locationString());
throw unexpectedTokenError("null");
}
}

Expand Down Expand Up @@ -915,14 +916,13 @@ public double nextDouble() throws IOException {
} else if (p == PEEKED_UNQUOTED) {
peekedString = nextUnquotedValue();
} else if (p != PEEKED_BUFFERED) {
throw new IllegalStateException("Expected a double but was " + peek() + locationString());
throw unexpectedTokenError("a double");
}

peeked = PEEKED_BUFFERED;
double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
throw new MalformedJsonException(
"JSON forbids NaN and infinities: " + result + locationString());
throw syntaxError("JSON forbids NaN and infinities: " + result);
}
peekedString = null;
peeked = PEEKED_NONE;
Expand Down Expand Up @@ -970,7 +970,7 @@ public long nextLong() throws IOException {
// Fall back to parse as a double below.
}
} else {
throw new IllegalStateException("Expected a long but was " + peek() + locationString());
throw unexpectedTokenError("a long");
}

peeked = PEEKED_BUFFERED;
Expand Down Expand Up @@ -1208,7 +1208,7 @@ public int nextInt() throws IOException {
// Fall back to parse as a double below.
}
} else {
throw new IllegalStateException("Expected an int but was " + peek() + locationString());
throw unexpectedTokenError("an int");
}

peeked = PEEKED_BUFFERED;
Expand Down Expand Up @@ -1584,10 +1584,10 @@ public String getPath() {
/**
* Unescapes the character identified by the character or characters that
* immediately follow a backslash. The backslash '\' should have already
* been read. This supports both unicode escapes "u000A" and two-character
* been read. This supports both Unicode escapes "u000A" and two-character
* escapes "\n".
*
* @throws MalformedJsonException if any unicode escape sequences are
* @throws MalformedJsonException if any Unicode escape sequences are
* malformed.
*/
@SuppressWarnings("fallthrough")
Expand All @@ -1614,7 +1614,7 @@ private char readEscapeCharacter() throws IOException {
} else if (c >= 'A' && c <= 'F') {
result += (c - 'A' + 10);
} else {
throw new MalformedJsonException("\\u" + new String(buffer, pos, 4));
throw syntaxError("Malformed Unicode escape \\u" + new String(buffer, pos, 4));
}
}
pos += 4;
Expand Down Expand Up @@ -1656,7 +1656,16 @@ private char readEscapeCharacter() throws IOException {
* with this reader's content.
*/
private IOException syntaxError(String message) throws IOException {
throw new MalformedJsonException(message + locationString());
throw new MalformedJsonException(message + locationString()
+ "\nSee " + TroubleshootingGuide.createUrl("malformed-json"));
}

private IllegalStateException unexpectedTokenError(String expected) throws IOException {
JsonToken peeked = peek();
String troubleshootingId = peeked == JsonToken.NULL
? "adapter-not-null-safe" : "unexpected-json-structure";
return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString()
+ "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId));
}

/**
Expand Down Expand Up @@ -1699,8 +1708,7 @@ private void consumeNonExecutePrefix() throws IOException {
} else if (p == PEEKED_UNQUOTED_NAME) {
reader.peeked = PEEKED_UNQUOTED;
} else {
throw new IllegalStateException(
"Expected a name but was " + reader.peek() + reader.locationString());
throw reader.unexpectedTokenError("a name");
}
}
};
Expand Down
25 changes: 8 additions & 17 deletions gson/src/test/java/com/google/gson/JsonArrayTest.java
Expand Up @@ -17,7 +17,6 @@
package com.google.gson;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;

import com.google.common.testing.EqualsTester;
Expand Down Expand Up @@ -140,22 +139,19 @@ public void testFailedGetArrayValues() {
jsonArray.getAsBoolean();
fail("expected getBoolean to fail");
} catch (UnsupportedOperationException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("JsonObject");
assertThat(e).hasMessageThat().isEqualTo("JsonObject");
}
try {
jsonArray.get(-1);
fail("expected get to fail");
} catch (IndexOutOfBoundsException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1");
assertThat(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1");
}
try {
jsonArray.getAsString();
fail("expected getString to fail");
} catch (UnsupportedOperationException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("JsonObject");
assertThat(e).hasMessageThat().isEqualTo("JsonObject");
}

jsonArray.remove(0);
Expand All @@ -164,36 +160,31 @@ public void testFailedGetArrayValues() {
jsonArray.getAsDouble();
fail("expected getDouble to fail");
} catch (NumberFormatException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
}
try {
jsonArray.getAsInt();
fail("expected getInt to fail");
} catch (NumberFormatException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
}
try {
jsonArray.get(0).getAsJsonArray();
fail("expected getJSONArray to fail");
} catch (IllegalStateException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\"");
assertThat(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\"");
}
try {
jsonArray.getAsJsonObject();
fail("expected getJSONObject to fail");
} catch (IllegalStateException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo( "Not a JSON Object: [\"hello\"]");
assertThat(e).hasMessageThat().isEqualTo("Not a JSON Object: [\"hello\"]");
}
try {
jsonArray.getAsLong();
fail("expected getLong to fail");
} catch (NumberFormatException e) {
assertWithMessage("Expected an exception message")
.that(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
}
}

Expand Down