Skip to content

Commit cbad1aa

Browse files
authored
Link to troubleshooting guide from exception messages (#2357)
* Link to troubleshooting guide from exception messages * Add examples to troubleshooting guide * Use proper anchor names for troubleshooting guide
1 parent 1717fd6 commit cbad1aa

26 files changed

Lines changed: 517 additions & 248 deletions

Troubleshooting.md

Lines changed: 93 additions & 22 deletions
Large diffs are not rendered by default.

gson/src/main/java/com/google/gson/Gson.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
* <p>See the <a href="https://github.com/google/gson/blob/master/UserGuide.md">Gson User Guide</a>
106106
* for a more complete set of examples.</p>
107107
*
108-
* <h2>Lenient JSON handling</h2>
108+
* <h2 id="default-lenient">Lenient JSON handling</h2>
109109
* For legacy reasons most of the {@code Gson} methods allow JSON data which does not
110110
* comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()}
111111
* is used or not. If this behavior is not desired, the following workarounds can be used:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.google.gson.internal;
2+
3+
public class TroubleshootingGuide {
4+
private TroubleshootingGuide() {}
5+
6+
/**
7+
* Creates a URL referring to the specified troubleshooting section.
8+
*/
9+
public static String createUrl(String id) {
10+
return "https://github.com/google/gson/blob/master/Troubleshooting.md#" + id;
11+
}
12+
}

gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.gson.internal.ObjectConstructor;
3434
import com.google.gson.internal.Primitives;
3535
import com.google.gson.internal.ReflectionAccessFilterHelper;
36+
import com.google.gson.internal.TroubleshootingGuide;
3637
import com.google.gson.internal.reflect.ReflectionHelper;
3738
import com.google.gson.reflect.TypeToken;
3839
import com.google.gson.stream.JsonReader;
@@ -114,7 +115,7 @@ public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
114115
if (filterResult == FilterResult.BLOCK_ALL) {
115116
throw new JsonIOException(
116117
"ReflectionAccessFilter does not permit using reflection for " + raw
117-
+ ". Register a TypeAdapter for this type or adjust the access filter.");
118+
+ ". Register a TypeAdapter for this type or adjust the access filter.");
118119
}
119120
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
120121

@@ -306,7 +307,8 @@ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type,
306307
if (previous != null) {
307308
throw new IllegalArgumentException("Class " + originalRaw.getName()
308309
+ " declares multiple JSON fields named '" + previous.name + "'; conflict is caused"
309-
+ " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field));
310+
+ " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field)
311+
+ "\nSee " + TroubleshootingGuide.createUrl("duplicate-fields"));
310312
}
311313
}
312314
type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));

gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.gson.TypeAdapterFactory;
2929
import com.google.gson.annotations.SerializedName;
3030
import com.google.gson.internal.LazilyParsedNumber;
31+
import com.google.gson.internal.TroubleshootingGuide;
3132
import com.google.gson.reflect.TypeToken;
3233
import com.google.gson.stream.JsonReader;
3334
import com.google.gson.stream.JsonToken;
@@ -73,12 +74,14 @@ private TypeAdapters() {
7374
@Override
7475
public void write(JsonWriter out, Class value) throws IOException {
7576
throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: "
76-
+ value.getName() + ". Forgot to register a type adapter?");
77+
+ value.getName() + ". Forgot to register a type adapter?"
78+
+ "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported"));
7779
}
7880
@Override
7981
public Class read(JsonReader in) throws IOException {
8082
throw new UnsupportedOperationException(
81-
"Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?");
83+
"Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"
84+
+ "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported"));
8285
}
8386
}.nullSafe();
8487

gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.gson.JsonIOException;
2020
import com.google.gson.internal.GsonBuildConfig;
21+
import com.google.gson.internal.TroubleshootingGuide;
2122
import java.lang.reflect.AccessibleObject;
2223
import java.lang.reflect.Constructor;
2324
import java.lang.reflect.Field;
@@ -40,6 +41,17 @@ public class ReflectionHelper {
4041

4142
private ReflectionHelper() {}
4243

44+
private static String getInaccessibleTroubleshootingSuffix(Exception e) {
45+
// Class was added in Java 9, therefore cannot use instanceof
46+
if (e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
47+
String message = e.getMessage();
48+
String troubleshootingId = message != null && message.contains("to module com.google.gson")
49+
? "reflection-inaccessible-to-module-gson" : "reflection-inaccessible";
50+
return "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId);
51+
}
52+
return "";
53+
}
54+
4355
/**
4456
* Internal implementation of making an {@link AccessibleObject} accessible.
4557
*
@@ -52,7 +64,8 @@ public static void makeAccessible(AccessibleObject object) throws JsonIOExceptio
5264
} catch (Exception exception) {
5365
String description = getAccessibleObjectDescription(object, false);
5466
throw new JsonIOException("Failed making " + description + " accessible; either increase its visibility"
55-
+ " or write a custom TypeAdapter for its declaring type.", exception);
67+
+ " or write a custom TypeAdapter for its declaring type." + getInaccessibleTroubleshootingSuffix(exception),
68+
exception);
5669
}
5770
}
5871

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

gson/src/main/java/com/google/gson/stream/JsonReader.java

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.gson.stream;
1818

1919
import com.google.gson.internal.JsonReaderInternalAccess;
20+
import com.google.gson.internal.TroubleshootingGuide;
2021
import com.google.gson.internal.bind.JsonTreeReader;
2122
import java.io.Closeable;
2223
import java.io.EOFException;
@@ -355,7 +356,7 @@ public void beginArray() throws IOException {
355356
pathIndices[stackSize - 1] = 0;
356357
peeked = PEEKED_NONE;
357358
} else {
358-
throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + locationString());
359+
throw unexpectedTokenError("BEGIN_ARRAY");
359360
}
360361
}
361362

@@ -373,7 +374,7 @@ public void endArray() throws IOException {
373374
pathIndices[stackSize - 1]++;
374375
peeked = PEEKED_NONE;
375376
} else {
376-
throw new IllegalStateException("Expected END_ARRAY but was " + peek() + locationString());
377+
throw unexpectedTokenError("END_ARRAY");
377378
}
378379
}
379380

@@ -390,7 +391,7 @@ public void beginObject() throws IOException {
390391
push(JsonScope.EMPTY_OBJECT);
391392
peeked = PEEKED_NONE;
392393
} else {
393-
throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() + locationString());
394+
throw unexpectedTokenError("BEGIN_OBJECT");
394395
}
395396
}
396397

@@ -409,7 +410,7 @@ public void endObject() throws IOException {
409410
pathIndices[stackSize - 1]++;
410411
peeked = PEEKED_NONE;
411412
} else {
412-
throw new IllegalStateException("Expected END_OBJECT but was " + peek() + locationString());
413+
throw unexpectedTokenError("END_OBJECT");
413414
}
414415
}
415416

@@ -797,7 +798,7 @@ public String nextName() throws IOException {
797798
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
798799
result = nextQuotedValue('"');
799800
} else {
800-
throw new IllegalStateException("Expected a name but was " + peek() + locationString());
801+
throw unexpectedTokenError("a name");
801802
}
802803
peeked = PEEKED_NONE;
803804
pathNames[stackSize - 1] = result;
@@ -833,7 +834,7 @@ public String nextString() throws IOException {
833834
result = new String(buffer, pos, peekedNumberLength);
834835
pos += peekedNumberLength;
835836
} else {
836-
throw new IllegalStateException("Expected a string but was " + peek() + locationString());
837+
throw unexpectedTokenError("a string");
837838
}
838839
peeked = PEEKED_NONE;
839840
pathIndices[stackSize - 1]++;
@@ -861,7 +862,7 @@ public boolean nextBoolean() throws IOException {
861862
pathIndices[stackSize - 1]++;
862863
return false;
863864
}
864-
throw new IllegalStateException("Expected a boolean but was " + peek() + locationString());
865+
throw unexpectedTokenError("a boolean");
865866
}
866867

867868
/**
@@ -880,7 +881,7 @@ public void nextNull() throws IOException {
880881
peeked = PEEKED_NONE;
881882
pathIndices[stackSize - 1]++;
882883
} else {
883-
throw new IllegalStateException("Expected null but was " + peek() + locationString());
884+
throw unexpectedTokenError("null");
884885
}
885886
}
886887

@@ -915,14 +916,13 @@ public double nextDouble() throws IOException {
915916
} else if (p == PEEKED_UNQUOTED) {
916917
peekedString = nextUnquotedValue();
917918
} else if (p != PEEKED_BUFFERED) {
918-
throw new IllegalStateException("Expected a double but was " + peek() + locationString());
919+
throw unexpectedTokenError("a double");
919920
}
920921

921922
peeked = PEEKED_BUFFERED;
922923
double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
923924
if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
924-
throw new MalformedJsonException(
925-
"JSON forbids NaN and infinities: " + result + locationString());
925+
throw syntaxError("JSON forbids NaN and infinities: " + result);
926926
}
927927
peekedString = null;
928928
peeked = PEEKED_NONE;
@@ -970,7 +970,7 @@ public long nextLong() throws IOException {
970970
// Fall back to parse as a double below.
971971
}
972972
} else {
973-
throw new IllegalStateException("Expected a long but was " + peek() + locationString());
973+
throw unexpectedTokenError("a long");
974974
}
975975

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

12141214
peeked = PEEKED_BUFFERED;
@@ -1584,10 +1584,10 @@ public String getPath() {
15841584
/**
15851585
* Unescapes the character identified by the character or characters that
15861586
* immediately follow a backslash. The backslash '\' should have already
1587-
* been read. This supports both unicode escapes "u000A" and two-character
1587+
* been read. This supports both Unicode escapes "u000A" and two-character
15881588
* escapes "\n".
15891589
*
1590-
* @throws MalformedJsonException if any unicode escape sequences are
1590+
* @throws MalformedJsonException if any Unicode escape sequences are
15911591
* malformed.
15921592
*/
15931593
@SuppressWarnings("fallthrough")
@@ -1614,7 +1614,7 @@ private char readEscapeCharacter() throws IOException {
16141614
} else if (c >= 'A' && c <= 'F') {
16151615
result += (c - 'A' + 10);
16161616
} else {
1617-
throw new MalformedJsonException("\\u" + new String(buffer, pos, 4));
1617+
throw syntaxError("Malformed Unicode escape \\u" + new String(buffer, pos, 4));
16181618
}
16191619
}
16201620
pos += 4;
@@ -1656,7 +1656,16 @@ private char readEscapeCharacter() throws IOException {
16561656
* with this reader's content.
16571657
*/
16581658
private IOException syntaxError(String message) throws IOException {
1659-
throw new MalformedJsonException(message + locationString());
1659+
throw new MalformedJsonException(message + locationString()
1660+
+ "\nSee " + TroubleshootingGuide.createUrl("malformed-json"));
1661+
}
1662+
1663+
private IllegalStateException unexpectedTokenError(String expected) throws IOException {
1664+
JsonToken peeked = peek();
1665+
String troubleshootingId = peeked == JsonToken.NULL
1666+
? "adapter-not-null-safe" : "unexpected-json-structure";
1667+
return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString()
1668+
+ "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId));
16601669
}
16611670

16621671
/**
@@ -1699,8 +1708,7 @@ private void consumeNonExecutePrefix() throws IOException {
16991708
} else if (p == PEEKED_UNQUOTED_NAME) {
17001709
reader.peeked = PEEKED_UNQUOTED;
17011710
} else {
1702-
throw new IllegalStateException(
1703-
"Expected a name but was " + reader.peek() + reader.locationString());
1711+
throw reader.unexpectedTokenError("a name");
17041712
}
17051713
}
17061714
};

gson/src/test/java/com/google/gson/JsonArrayTest.java

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.google.gson;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20-
import static com.google.common.truth.Truth.assertWithMessage;
2120
import static org.junit.Assert.fail;
2221

2322
import com.google.common.testing.EqualsTester;
@@ -140,22 +139,19 @@ public void testFailedGetArrayValues() {
140139
jsonArray.getAsBoolean();
141140
fail("expected getBoolean to fail");
142141
} catch (UnsupportedOperationException e) {
143-
assertWithMessage("Expected an exception message")
144-
.that(e).hasMessageThat().isEqualTo("JsonObject");
142+
assertThat(e).hasMessageThat().isEqualTo("JsonObject");
145143
}
146144
try {
147145
jsonArray.get(-1);
148146
fail("expected get to fail");
149147
} catch (IndexOutOfBoundsException e) {
150-
assertWithMessage("Expected an exception message")
151-
.that(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1");
148+
assertThat(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1");
152149
}
153150
try {
154151
jsonArray.getAsString();
155152
fail("expected getString to fail");
156153
} catch (UnsupportedOperationException e) {
157-
assertWithMessage("Expected an exception message")
158-
.that(e).hasMessageThat().isEqualTo("JsonObject");
154+
assertThat(e).hasMessageThat().isEqualTo("JsonObject");
159155
}
160156

161157
jsonArray.remove(0);
@@ -164,36 +160,31 @@ public void testFailedGetArrayValues() {
164160
jsonArray.getAsDouble();
165161
fail("expected getDouble to fail");
166162
} catch (NumberFormatException e) {
167-
assertWithMessage("Expected an exception message")
168-
.that(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
163+
assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
169164
}
170165
try {
171166
jsonArray.getAsInt();
172167
fail("expected getInt to fail");
173168
} catch (NumberFormatException e) {
174-
assertWithMessage("Expected an exception message")
175-
.that(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
169+
assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
176170
}
177171
try {
178172
jsonArray.get(0).getAsJsonArray();
179173
fail("expected getJSONArray to fail");
180174
} catch (IllegalStateException e) {
181-
assertWithMessage("Expected an exception message")
182-
.that(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\"");
175+
assertThat(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\"");
183176
}
184177
try {
185178
jsonArray.getAsJsonObject();
186179
fail("expected getJSONObject to fail");
187180
} catch (IllegalStateException e) {
188-
assertWithMessage("Expected an exception message")
189-
.that(e).hasMessageThat().isEqualTo( "Not a JSON Object: [\"hello\"]");
181+
assertThat(e).hasMessageThat().isEqualTo("Not a JSON Object: [\"hello\"]");
190182
}
191183
try {
192184
jsonArray.getAsLong();
193185
fail("expected getLong to fail");
194186
} catch (NumberFormatException e) {
195-
assertWithMessage("Expected an exception message")
196-
.that(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
187+
assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
197188
}
198189
}
199190

0 commit comments

Comments
 (0)