iOfficeAI/OfficeCLI · error · JsonException

Unexpected end of JSON for BatchItem

Error message

Unexpected end of JSON for BatchItem

What it means

Thrown by the custom JsonConverter<BatchItem>.Read in BatchTypes.cs when the JSON reader reaches end-of-input before finding the closing `}` of a batch item object. The reader loop (`while (reader.Read())`) returns false on truncation, falls through the property switch, and hits the guard at line 112. It means the batch JSON stream was cut off mid-object — a malformed or incompletely-written input.

Source

Thrown at src/officecli/BatchTypes.cs:112

                case "index": item.Index = reader.TokenType == JsonTokenType.Null ? null : reader.GetInt32(); break;
                case "after": item.After = reader.GetString(); break;
                case "before": item.Before = reader.GetString(); break;
                case "to": item.To = reader.GetString(); break;
                case "path2": item.Path2 = reader.GetString(); break;
                case "props": item.Props = PropsConverter.Read(ref reader, typeof(Dictionary<string, string>), options); break;
                case "selector": item.Selector = reader.GetString(); break;
                case "text": item.Text = reader.GetString(); break;
                case "mode": item.Mode = reader.GetString(); break;
                case "depth": item.Depth = reader.TokenType == JsonTokenType.Null ? null : reader.GetInt32(); break;
                case "part": item.Part = reader.GetString(); break;
                case "xpath": item.Xpath = reader.GetString(); break;
                case "action": item.Action = reader.GetString(); break;
                case "xml": item.Xml = reader.GetString(); break;
                case "dumpversion": item.DumpVersion = reader.TokenType == JsonTokenType.Null ? null : reader.GetInt32(); break;
                default: reader.Skip(); break;
            }
        }
        throw new JsonException("Unexpected end of JSON for BatchItem");
    }

    public override void Write(Utf8JsonWriter writer, BatchItem value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        if (!string.IsNullOrEmpty(value.Command)) writer.WriteString("command", value.Command);
        if (value.Path != null) writer.WriteString("path", value.Path);
        if (value.Parent != null) writer.WriteString("parent", value.Parent);
        if (value.Type != null) writer.WriteString("type", value.Type);
        if (value.From != null) writer.WriteString("from", value.From);
        if (value.Index.HasValue) writer.WriteNumber("index", value.Index.Value);
        if (value.After != null) writer.WriteString("after", value.After);
        if (value.Before != null) writer.WriteString("before", value.Before);
        if (value.To != null) writer.WriteString("to", value.To);
        if (value.Path2 != null) writer.WriteString("path2", value.Path2);
        if (value.Props != null) { writer.WritePropertyName("props"); PropsConverter.Write(writer, value.Props, options); }
        if (value.Selector != null) writer.WriteString("selector", value.Selector);
        if (value.Text != null) writer.WriteString("text", value.Text);

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Inspect the input source end-to-end: `tail -c 50 ops.json` to confirm it ends with `]` and the last object closes with `}`.
  2. Validate the file is complete JSON before invoking batch: `jq empty ops.json` (jq exits non-zero on truncation).
  3. If reading from stdin, verify the producer writes the full payload and closes the pipe; redirect to a file first and check its size/ending.
  4. Regenerate the batch ops file from `dump` or your script so it is well-formed.

Example fix

// before (truncated)
[{"command":"get","path":"/

// after (complete)
[{"command":"get","path":"/"}]
Defensive patterns

Strategy: validation

Validate before calling

// Validate the batch JSON is complete and well-formed before invoking batch.
using var doc = System.Text.Json.JsonDocument.Parse(jsonText);
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Array)
    throw new InvalidOperationException("batch input must be a JSON array");
foreach (var elem in doc.RootElement.EnumerateArray())
{
    if (elem.ValueKind != System.Text.Json.JsonValueKind.Object
        && elem.ValueKind != System.Text.Json.JsonValueKind.Null)
        throw new InvalidOperationException("batch array entries must be objects");
}
// Shell equivalent before calling officecli:
//   jq empty ops.json && officecli batch foo.docx --input ops.json

Type guard

// C# guard that a JSON string is a complete array of objects/nulls.
static bool IsCompleteBatchArray(string json)
{
    try
    {
        using var d = System.Text.Json.JsonDocument.Parse(json);
        if (d.RootElement.ValueKind != System.Text.Json.JsonValueKind.Array) return false;
        foreach (var e in d.RootElement.EnumerateArray())
            if (e.ValueKind != System.Text.Json.JsonValueKind.Object
                && e.ValueKind != System.Text.Json.JsonValueKind.Null) return false;
        return true;
    }
    catch { return false; }
}

Prevention

When it happens

Trigger: `officecli batch foo.docx --input ops.json` where ops.json is truncated like `[{"command":"get","path":"/"` (no closing brace). Also reachable when stdin is a broken pipe (`cat ops.json | head -c 100 | officecli batch foo.docx`), when a file write was interrupted, or when an AI agent streams a partial JSON object and the connection drops before completion.

Common situations: A batch script writes the JSON file and crashes mid-write; a network copy of the ops file is incomplete; an LLM agent emits a JSON array but truncates the last object; piping through a tool that mangles/buffers and cuts the stream; editing the JSON by hand and forgetting the final `}`.

Understand the failure class

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/a8acd6c9921f3e21. Report an issue: GitHub.