elastic/elasticsearch · error · IOException

Unclosed object or array found

Error message

Unclosed object or array found

What it means

On close(), JsonXContentGenerator inspects the Jackson output context. If the context is not in the root state (i.e. there is an unclosed object or array), it throws IOException. This enforces structural completeness before the underlying generator is closed, so a half-written document fails loudly rather than producing malformed JSON.

Source

Thrown at libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/json/JsonXContentGenerator.java:610

        flush();
        writer.accept(os);
        flush();
        writeEndRaw();
    }

    @Override
    public void flush() throws IOException {
        generator.flush();
    }

    @Override
    public void close() throws IOException {
        if (generator.isClosed()) {
            return;
        }
        JsonStreamContext context = generator.getOutputContext();
        if ((context != null) && (context.inRoot() == false)) {
            throw new IOException("Unclosed object or array found");
        }
        if (writeLineFeedAtEnd) {
            flush();
            // Bypass generator to always write the line feed
            getLowLevelGenerator().writeRaw(LF);
        }
        generator.close();
    }

    @Override
    public boolean isClosed() {
        return generator.isClosed();
    }
}

View on GitHub (pinned to db6a809a66)

Solutions

  1. Always pair writeStartObject/writeStartArray with the matching writeEndObject/writeEndArray in finally blocks or use try-with-resources around the structure
  2. Use a stack-based or block-scoped helper that guarantees the end call
  3. Re-run with a JSON validator to find the unbalanced structure

Example fix

// before
generator.writeStartObject();
generator.writeStartArray("items");
generator.writeEndArray();
// missing writeEndObject()
generator.close(); // throws
// after
generator.writeStartObject();
generator.writeStartArray("items");
generator.writeEndArray();
generator.writeEndObject();
generator.close();
Defensive patterns

Strategy: try-catch

Validate before calling

// structural pairing helper
try (var ignored = new JsonBlock(generator)) {
    generator.writeStartObject();
    // ...
} // close calls writeEndObject if not yet closed

Type guard

// no language-level type guard; track open depth in a counter

Try / catch

try (JsonXContentGenerator g = ...) {
    g.writeStartObject();
    ...
} catch (IOException e) {
    // 'Unclosed object or array found' on close
}

Prevention

When it happens

Trigger: Calling generator.close() (or the try-with-resources exit) while one or more writeStartObject()/writeStartArray() calls were not matched by writeEndObject()/writeEndArray().

Common situations: A branch returned or threw before the matching end call; an exception during writing was swallowed and close() ran on an unbalanced context; refactoring added a new startObject but missed the end; conditional writes that start a structure on one path but not the other.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/3fa85fc99bd10932. Report an issue: GitHub.