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

  1. Ensure every beginObject() has a matching endObject() and every beginArray() a matching endArray() before calling get()
  2. Wrap begin/end in try-finally so containers close even on an exception path
  3. 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

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


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)