{"record":{"id":"7790258cc6963b02","repo":"iOfficeAI/OfficeCLI","slug":"expected-key-value-string-in-props-array","errorCode":null,"errorMessage":"Expected \"key=value\" string in props array","messagePattern":"Expected \"key=value\" string in props array","errorType":"validation","errorClass":"JsonException","httpStatus":null,"severity":"warning","filePath":"src/officecli/BatchTypes.cs","lineNumber":28,"sourceCode":"{\n    public override Dictionary<string, string>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        if (reader.TokenType == JsonTokenType.Null) return null;\n        var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n        // Array form: [\"key=value\", ...]. This mirrors the single-command MCP\n        // `props` argument and the CLI `--prop key=value` flag, so an agent that\n        // learned props from `set`/`add` produces the same shape inside a batch\n        // item. Before this, batch props was object-only and every array-form\n        // batch failed with \"Expected object for props\" — observed as a 100%\n        // batch-failure for models that (correctly) reused the single-command\n        // props shape. Lenient split on the first '=' matches McpServer.ParseProps.\n        if (reader.TokenType == JsonTokenType.StartArray)\n        {\n            while (reader.Read())\n            {\n                if (reader.TokenType == JsonTokenType.EndArray) return dict;\n                if (reader.TokenType != JsonTokenType.String)\n                    throw new JsonException(\"Expected \\\"key=value\\\" string in props array\");\n                var kv = reader.GetString()!;\n                var eq = kv.IndexOf('=');\n                if (eq > 0) dict[kv[..eq]] = kv[(eq + 1)..];  // skip malformed, as ParseProps does\n            }\n            throw new JsonException(\"Unexpected end of JSON\");\n        }\n        if (reader.TokenType != JsonTokenType.StartObject)\n            throw new JsonException(\"Expected object or [\\\"key=value\\\"] array for props\");\n        while (reader.Read())\n        {\n            if (reader.TokenType == JsonTokenType.EndObject) return dict;\n            if (reader.TokenType != JsonTokenType.PropertyName)\n                throw new JsonException(\"Expected property name\");\n            var key = reader.GetString()!;\n            reader.Read();\n            var value = reader.TokenType switch\n            {\n                JsonTokenType.String => reader.GetString()!,","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/iOfficeAI/OfficeCLI/blob/1ced45e900782c5083ed550ddf328ee974e425e7/src/officecli/BatchTypes.cs#L10-L46","documentation":"JsonException from LenientStringDictionaryConverter.Read while parsing a `props` value given as an array (the [\"key=value\", ...] form). Each element must be a JSON string; this throws when an element is a number, boolean, object, or nested array. It mirrors McpServer.ParseProps, which only accepts string kv-pairs in the array form.","triggerScenarios":"A batch item with \"props\": [\"text=hi\", 42] or \"props\": [\"text=hi\", {\"x\":1}] or \"props\": [true]. Any non-string token inside the props array.","commonSituations":"An agent/model that emits props as a mixed array after learning a numeric value; serializing a Map/struct directly into the array; hand-built JSON with an unquoted value.","solutions":["Make every props-array element a \"key=value\" string: props: [\"text=hi\", \"count=42\"].","If you have rich values, switch to the object form: props: { text: 'hi', count: '42' }.","Validate/normalize props to strings before serializing the batch."],"exampleFix":"// before: { command:'set', props: ['text=hi', 42] }\n// after:  { command:'set', props: ['text=hi', 'count=42'] }  // or props: { text:'hi', count:'42' }","handlingStrategy":"validation","validationCode":"// Normalize props to the accepted shapes before serializing a batch\nfunction normalizeProps(props) {\n  if (props == null) return undefined;\n  if (Array.isArray(props)) return props.map(v => typeof v === 'string' ? v : null).filter(Boolean); // drop non-strings\n  if (typeof props === 'object') return props;\n  return undefined;\n}","typeGuard":"// Accept only string elements in a props array\nfunction isStringArray(a) { return Array.isArray(a) && a.every(x => typeof x === 'string'); }","tryCatchPattern":null,"preventionTips":["Always quote props-array values as \"key=value\" strings.","Prefer the object form {key:'value'} unless you specifically need the array form.","Coerce numbers to strings before pushing into a props array."],"tags":["batch","json","props","deserialization","api-contract"],"backgroundTag":null,"analyzedSha":"1ced45e900782c5083ed550ddf328ee974e425e7","analyzedAt":"2026-08-13T13:01:07.193Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}