google/gson · error · IOException

Incomplete document

Error message

Incomplete document

What it means

Thrown by JsonWriter.close() (an IOException, not IllegalStateException) when the writer's internal scope stack is not in a terminal state: either open containers remain (stackSize > 1) or a single frame exists but it is not NONEMPTY_DOCUMENT (meaning no top-level value was ever written, or an array/object was opened and the document ended mid-stream). The check lives at JsonWriter.java:723-725. It enforces that close() is only valid after exactly one complete top-level JSON value, per RFC 8259.

Solutions

  1. Ensure every beginArray()/beginObject() has a matching endArray()/endObject() before close(); structure the writing block so the closes run in a finally that only executes after the opens.
  2. If using try-with-resources, make sure the document body is fully constructed inside the try block and close happens only after the top-level value is complete.
  3. Verify a top-level value is actually written: a brand-new writer closed immediately throws because the single frame is EMPTY_DOCUMENT, not NONEMPTY_DOCUMENT.
  4. Track depth with a counter or use a helper that auto-balances scopes if you cannot guarantee symmetric calls on error paths.

Example fix

// before
JsonWriter w = new JsonWriter(new StringWriter());
w.beginObject();
w.name("a").value(1);
w.close(); // throws: object never closed

// after
JsonWriter w = new JsonWriter(new StringWriter());
w.beginObject();
w.name("a").value(1);
w.endObject();
w.close();
Defensive patterns

Strategy: validation

Validate before calling

// Cannot query stack depth directly; mirror scope depth yourself
class JsonWriterGuard {
  private final JsonWriter w;
  private int depth = 0; // open containers
  private boolean wroteTopLevel = false;
  JsonWriterGuard(JsonWriter w) { this.w = w; }
  void openArray() throws IOException { w.beginArray(); depth++; }
  void openObject() throws IOException { w.beginObject(); depth++; }
  void closeArray() throws IOException { w.endArray(); depth--; }
  void closeObject() throws IOException { w.endObject(); depth--; }
  void markValue() throws IOException { /* call after each value */ if (depth == 0) wroteTopLevel = true; }
  /** Call before close(): returns false if close() would throw 'Incomplete document'. */
  boolean isSafeToClose() { return depth == 0 && wroteTopLevel; }
}

Type guard

null

Try / catch

// close() throws IOException; ensure scopes balanced in finally
try (JsonWriter w = new JsonWriter(out)) {
  w.beginObject();
  try {
    w.name("k").value(1);
  } finally {
    w.endObject(); // guarantee container close before outer close
  }
} catch (IOException e) {
  if (e.getMessage() != null && e.getMessage().startsWith("Incomplete document")) {
    // log and surface as a serialization bug, not a clean shutdown
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling writer.close() without having written any value at all (fresh writer, stack still EMPTY_DOCUMENT); calling close() after beginArray()/beginObject() without the matching endArray()/endObject(); calling close() after writing a single literal but the stream still has an unclosed container; forgetting to wrap a top-level value so the stack frame is still EMPTY_DOCUMENT at close time.

Common situations: Early-return or exception paths in serialization code that skip the endObject()/endArray() calls but still close() the writer (try-with-resources closing on a partially-built document); recursive serializers that bail out partway; refactoring that adds beginObject() without updating the finally block; writing into an empty StringWriter for a DTO that serializes to nothing.

Related errors


AI-assisted analysis of google/gson@310ac341f2 (2026-08-10). Data as JSON: /api/errors/3823de80e6ba1c37. Report an issue: GitHub.

Appendix: source

Thrown at gson/src/main/java/com/google/gson/stream/JsonWriter.java:724

  public void flush() throws IOException {
    if (stackSize == 0) {
      throw new IllegalStateException("JsonWriter is closed.");
    }
    out.flush();
  }

  /**
   * Flushes and closes this writer and the underlying {@link Writer}.
   *
   * @throws IOException if the JSON document is incomplete.
   */
  @Override
  public void close() throws IOException {
    out.close();

    int size = stackSize;
    if (size > 1 || (size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT)) {
      throw new IOException("Incomplete document");
    }
    stackSize = 0;
  }

  /** Returns whether the {@code toString()} of {@code c} will always return a valid JSON number. */
  private static boolean alwaysCreatesValidJsonNumber(Class<? extends Number> c) {
    // Does not include Float or Double because their value can be NaN or Infinity
    // Does not include LazilyParsedNumber because it could contain a malformed string
    return c == Integer.class
        || c == Long.class
        || c == Byte.class
        || c == Short.class
        || c == BigDecimal.class
        || c == BigInteger.class
        || c == AtomicInteger.class
        || c == AtomicLong.class;
  }

View on GitHub (pinned to 310ac341f2)