{"record":{"id":"87930cfe8c380d51","repo":"apache/shenyu","slug":"invalid-request-body-expected-a-json-object","errorCode":null,"errorMessage":"Invalid request body: expected a JSON object","messagePattern":"Invalid request body: expected a JSON object","errorType":"validation","errorClass":"ShenyuException","httpStatus":null,"severity":"error","filePath":"shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-common/src/main/java/org/apache/shenyu/plugin/ai/common/protocol/OpenAiProtocolAdapter.java","lineNumber":123,"sourceCode":"    /**\n     * Parse raw request body directly into ChatCompletionRequest, preserving modeled fields.\n     * When fallbackConfig is provided, its non-null fields (model, temperature, maxTokens)\n     * override the client request values, matching the original ChatModel-based fallback behavior\n     * where the fallback ChatModel's config takes precedence.\n     *\n     * @param requestBody     the raw JSON request body in OpenAI Chat Completions format\n     * @param stream          whether this is a streaming request\n     * @param fallbackConfig  the fallback config whose non-null fields override client values\n     * @return a ChatCompletionRequest with modeled fields preserved\n     */\n    public static ChatCompletionRequest toChatCompletionRequest(final String requestBody,\n            final boolean stream, final AiCommonConfig fallbackConfig) {\n        if (Objects.isNull(requestBody) || requestBody.isEmpty()) {\n            throw new ShenyuException(\"Request body must not be empty\");\n        }\n        final JsonNode root = parseStrict(requestBody);\n        if (Objects.isNull(root) || !root.isObject()) {\n            throw new ShenyuException(\"Invalid request body: expected a JSON object\");\n        }\n        final ObjectNode mutableRoot = (ObjectNode) root;\n\n        if (Objects.nonNull(fallbackConfig)) {\n            if (Objects.nonNull(fallbackConfig.getModel()) && !fallbackConfig.getModel().isEmpty()) {\n                mutableRoot.put(FIELD_MODEL, fallbackConfig.getModel());\n            }\n            if (Objects.nonNull(fallbackConfig.getTemperature())) {\n                mutableRoot.put(FIELD_TEMPERATURE, fallbackConfig.getTemperature());\n            }\n            if (Objects.nonNull(fallbackConfig.getMaxTokens())) {\n                mutableRoot.put(FIELD_MAX_TOKENS, fallbackConfig.getMaxTokens());\n                mutableRoot.put(FIELD_MAX_COMPLETION_TOKENS, fallbackConfig.getMaxTokens());\n            }\n        }\n\n        mutableRoot.put(FIELD_STREAM, stream);\n","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/apache/shenyu/blob/567142e07261b3e615ae8850b30f4421f455cc5d/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-common/src/main/java/org/apache/shenyu/plugin/ai/common/protocol/OpenAiProtocolAdapter.java#L105-L141","documentation":"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.","triggerScenarios":"Posting '[...]', '\"text\"', '42', or any malformed JSON to the AI chat completion endpoint and having it adapted via toChatCompletionRequest.","commonSituations":"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.","solutions":["Send a JSON object at the top level: {\"model\":..., \"messages\":[...]}","Set Content-Type: application/json on the client request","Validate the payload shape before sending; parseStrict rejects arrays/scalars by design","Check for middleware/proxies altering the request body"],"exampleFix":"// before\ncurl -d '[{\"role\":\"user\",\"content\":\"hi\"}]' ...\n// after\ncurl -H 'Content-Type: application/json' -d '{\"model\":\"gpt-4o\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}' ...","handlingStrategy":"validation","validationCode":"try {\n    JsonNode n = new ObjectMapper().readTree(requestBody);\n    if (n == null || !n.isObject()) throw new IllegalArgumentException(\"body must be a JSON object\");\n} catch (JsonProcessingException e) {\n    throw new IllegalArgumentException(\"body is not valid JSON\", e);\n}","typeGuard":null,"tryCatchPattern":"try {\n    ChatCompletionRequest req = OpenAiProtocolAdapter.toChatCompletionRequest(body, stream, config);\n} catch (ShenyuException e) {\n    if (e.getMessage().contains(\"expected a JSON object\")) {\n        return ResponseEntity.badRequest().body(\"top-level JSON object required\");\n    }\n    throw e;\n}","preventionTips":["Send Content-Type: application/json","Ensure the top-level payload is an object, not an array","Add a strict JSON-object pre-check in the plugin chain","Test endpoints with curl using well-formed payloads"],"tags":["ai","json","validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"567142e07261b3e615ae8850b30f4421f455cc5d","analyzedAt":"2026-09-12T10:08:21.293Z","contentChangedAt":"2026-09-12T10:08:21.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}