apache/shenyu · error · ShenyuException

Invalid request body: expected a JSON object

Error message

Invalid request body: expected a JSON object

What it means

After confirming the body is non-empty, toChatCompletionRequest parses it strictly with parseStrict. If the result is null or not a JSON object (e.g. a JSON array, string, or number at the top level), it throws ShenyuException('Invalid request body: expected a JSON object'). OpenAI chat completion payloads must be JSON objects.

Solutions

  1. Send a JSON object at the top level: {"model":..., "messages":[...]}
  2. Set Content-Type: application/json on the client request
  3. Validate the payload shape before sending; parseStrict rejects arrays/scalars by design
  4. Check for middleware/proxies altering the request body

Example fix

// before
curl -d '[{"role":"user","content":"hi"}]' ...
// after
curl -H 'Content-Type: application/json' -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}' ...
Defensive patterns

Strategy: validation

Validate before calling

try {
    JsonNode n = new ObjectMapper().readTree(requestBody);
    if (n == null || !n.isObject()) throw new IllegalArgumentException("body must be a JSON object");
} catch (JsonProcessingException e) {
    throw new IllegalArgumentException("body is not valid JSON", e);
}

Try / catch

try {
    ChatCompletionRequest req = OpenAiProtocolAdapter.toChatCompletionRequest(body, stream, config);
} catch (ShenyuException e) {
    if (e.getMessage().contains("expected a JSON object")) {
        return ResponseEntity.badRequest().body("top-level JSON object required");
    }
    throw e;
}

Prevention

When it happens

Trigger: Posting '[...]', '"text"', '42', or any malformed JSON to the AI chat completion endpoint and having it adapted via toChatCompletionRequest.

Common situations: Clients sending wrong Content-Type bodies (form-encoded ending up quoted), sending an array of messages at top level instead of an object with 'messages', proxies mangling the body, curl tests without a proper JSON payload.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of apache/shenyu@567142e072 (2026-09-12). Data as JSON: /api/errors/87930cfe8c380d51. Report an issue: GitHub.

Appendix: source

Thrown at shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-common/src/main/java/org/apache/shenyu/plugin/ai/common/protocol/OpenAiProtocolAdapter.java:123

    /**
     * Parse raw request body directly into ChatCompletionRequest, preserving modeled fields.
     * When fallbackConfig is provided, its non-null fields (model, temperature, maxTokens)
     * override the client request values, matching the original ChatModel-based fallback behavior
     * where the fallback ChatModel's config takes precedence.
     *
     * @param requestBody     the raw JSON request body in OpenAI Chat Completions format
     * @param stream          whether this is a streaming request
     * @param fallbackConfig  the fallback config whose non-null fields override client values
     * @return a ChatCompletionRequest with modeled fields preserved
     */
    public static ChatCompletionRequest toChatCompletionRequest(final String requestBody,
            final boolean stream, final AiCommonConfig fallbackConfig) {
        if (Objects.isNull(requestBody) || requestBody.isEmpty()) {
            throw new ShenyuException("Request body must not be empty");
        }
        final JsonNode root = parseStrict(requestBody);
        if (Objects.isNull(root) || !root.isObject()) {
            throw new ShenyuException("Invalid request body: expected a JSON object");
        }
        final ObjectNode mutableRoot = (ObjectNode) root;

        if (Objects.nonNull(fallbackConfig)) {
            if (Objects.nonNull(fallbackConfig.getModel()) && !fallbackConfig.getModel().isEmpty()) {
                mutableRoot.put(FIELD_MODEL, fallbackConfig.getModel());
            }
            if (Objects.nonNull(fallbackConfig.getTemperature())) {
                mutableRoot.put(FIELD_TEMPERATURE, fallbackConfig.getTemperature());
            }
            if (Objects.nonNull(fallbackConfig.getMaxTokens())) {
                mutableRoot.put(FIELD_MAX_TOKENS, fallbackConfig.getMaxTokens());
                mutableRoot.put(FIELD_MAX_COMPLETION_TOKENS, fallbackConfig.getMaxTokens());
            }
        }

        mutableRoot.put(FIELD_STREAM, stream);

View on GitHub (pinned to 567142e072)