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
- 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.
- 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.
- 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.
- 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
- Match every beginArray/beginObject with endArray/endObject inside the same try block, ideally in a finally.
- Never let a value/begin call leak across a close() boundary; close only after the top-level value is complete.
- Write a thin wrapper that tracks open-scope depth and refuses close() unless depth==0 and at least one value was written.
- Unit-test serializers with an empty input and a nested input to catch unbalanced scope paths.
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
- JSON must have only one top-level value.
- See
- Already wrote a name, expecting a value.
- Dangling name
- Did not consume the entire document.
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)