github/copilot-sdk · error · InvalidOperationException

Expected JSON object for `params` of single-object-param…

Error message

Expected JSON object for `params` of single-object-param handler; got '{paramsProp.ValueKind}'.

What it means

InvokeHandlerAsync throws InvalidOperationException when a handler registered as taking a single object parameter receives a `params` value that is not a JSON object (e.g. array, string, or null). These handlers deserialize the entire params object into the first parameter, so a non-object shape cannot be bound.

Solutions

  1. Send params as a JSON object with the field names the TRequest type expects.
  2. Omit positional-array params; convert them to named-object params on the client.
  3. Ensure client and server agree on the method's request schema (update the client SDK).
  4. For optional params, send an empty object rather than null/omitted params if the handler requires an object.

Example fix

// before
{"jsonrpc":"2.0","id":1,"method":"initialize","params":[]}
// after
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"x","version":"1"}}}
Defensive patterns

Strategy: validation

Validate before calling

static JsonNode? ValidateParams(JsonElement? raw) =>
    raw is { ValueKind: JsonValueKind.Object } el ? JsonNode.Parse(el.GetRawText())
    : throw new ArgumentException("params must be a JSON object for this method");

Type guard

bool IsJsonObjectParams(JsonElement p) => p.ValueKind == JsonValueKind.Object;

Try / catch

try { result = await rpc.InvokeAsync(method, requestObj, ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("single-object-param handler")) {
    logger.LogError(ex, "Client sent non-object params for {Method}", method);
    return JsonRpcError.InvalidParams(ex.Message);
}

Prevention

When it happens

Trigger: Client sends params as an array (positional style) or omits params (ValueKind Undefined/Null) for a method whose handler is a (TRequest, CancellationToken) single-object handler.

Common situations: Client SDK using positional parameters while server method expects named fields; a request built by hand with "params": [] or no params; protocol mismatch between client and server versions.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/d9790715d7723176. Report an issue: GitHub.

Appendix: source

Thrown at dotnet/src/JsonRpc.cs:616

            }
        }
    }

    private async ValueTask<object?> InvokeHandlerAsync(MethodRegistration registration, JsonElement paramsProp, CancellationToken cancellationToken)
    {
        var parameters = registration.Parameters;

        // Build argument list
        var invokeArgs = new object?[parameters.Length];

        if (registration.SingleObjectParam)
        {
            // Single-object deserialization: entire `params` → first parameter.
            // Every singleObjectParam handler has shape (TRequest, CancellationToken),
            // so `params` must be a JSON object.
            if (paramsProp.ValueKind != JsonValueKind.Object)
            {
                throw new InvalidOperationException(
                    $"Expected JSON object for `params` of single-object-param handler; got '{paramsProp.ValueKind}'.");
            }

            for (int i = 0; i < parameters.Length; i++)
            {
                if (parameters[i].ParameterType == typeof(CancellationToken))
                {
                    invokeArgs[i] = cancellationToken;
                }
                else if (i == 0)
                {
                    invokeArgs[i] = paramsProp.Deserialize(_serializerOptions.GetTypeInfo(parameters[i].ParameterType));
                }
            }
        }
        else if (paramsProp.ValueKind == JsonValueKind.Array)
        {
            // Positional parameters. Optional params (with defaults) are filled when absent.

View on GitHub (pinned to cd8cf15dc3)