google/gson · error · IllegalStateException
Expected one JSON element but was
Error message
Expected one JSON element but was ${stack} What it means
JsonTreeWriter builds a JsonElement tree and get() returns the finished product only when its internal container stack is empty - i.e. every beginObject()/beginArray() has been matched by an endObject()/endArray(). If the stack is non-empty when get() is called, an open container remains unclosed and the document is structurally incomplete, so Gson throws IllegalStateException. This is typically reached via TypeAdapter.toJsonTree() or when a custom adapter writes to a JsonTreeWriter.
Solutions
- Ensure every beginObject() has a matching endObject() and every beginArray() a matching endArray() before calling get()
- Wrap begin/end in try-finally so containers close even on an exception path
- Prefer Gson.toJsonTree() over manual JsonTreeWriter use so lifecycle is managed internally
Example fix
// before - stack not empty, get() throws
writer.beginObject();
writer.name("a").value(1);
return writer.get();
// after - close the container first
writer.beginObject();
writer.name("a").value(1);
writer.endObject();
return writer.get(); Defensive patterns
Strategy: validation
Validate before calling
// Track open-container depth and assert it is zero before get()
int depth = openContainers; // your counter
if (depth != 0) {
throw new IllegalStateException("Cannot get(): " + depth + " open containers");
}
JsonElement result = writer.get(); Try / catch
try {
JsonElement el = writer.get();
} catch (IllegalStateException e) {
// 'Expected one JSON element but was [...]': an open container remains;
// close it or discard the writer and rebuild the tree
} Prevention
- Always pair beginObject/beginArray with matching end calls
- Use try-finally to guarantee end calls run on exception paths
- Prefer Gson.toJsonTree() which manages JsonTreeWriter lifecycle internally
When it happens
Trigger: Calling writer.get() after beginObject()/beginArray() without the matching end call; a custom TypeAdapter.write() that opens containers but returns (or throws) before closing them; conditional code that begins a container on one branch but forgets to end it.
Common situations: Custom TypeAdapter with an early return or exception that skips endObject(); copy-paste bug omitting the closing call; logic that opens nested containers only conditionally.
Related errors
- Did not expect a name
- Please begin an object before writing a name.
- Incomplete document
- Already wrote a name, expecting a value.
- Dangling name
AI-assisted analysis of google/gson@310ac341f2 (2026-08-10).
Data as JSON: /api/errors/b7595a730ba4f30b.
Report an issue: GitHub.
Appendix: source
Thrown at gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java:71
private static final JsonPrimitive SENTINEL_CLOSED = new JsonPrimitive("closed");
/** The JsonElements and JsonArrays under modification, outermost to innermost. */
private final List<JsonElement> stack = new ArrayList<>();
/** The name for the next JSON object value. If non-null, the top of the stack is a JsonObject. */
private String pendingName;
/** the JSON element constructed by this writer. */
private JsonElement product = JsonNull.INSTANCE; // TODO: is this really what we want?;
public JsonTreeWriter() {
super(UNWRITABLE_WRITER);
}
/** Returns the top level object produced by this writer. */
public JsonElement get() {
if (!stack.isEmpty()) {
throw new IllegalStateException("Expected one JSON element but was " + stack);
}
return product;
}
private JsonElement peek() {
return stack.get(stack.size() - 1);
}
private void put(JsonElement value) {
if (pendingName != null) {
if (!value.isJsonNull() || getSerializeNulls()) {
JsonObject object = (JsonObject) peek();
object.add(pendingName, value);
}
pendingName = null;
} else if (stack.isEmpty()) {
product = value;
} else {View on GitHub (pinned to 310ac341f2)