{"record":{"id":"469447b4307dbf73","repo":"grpc-ecosystem/grpc-gateway","slug":"s-expected-json-object-at-top-level","errorCode":null,"errorMessage":"%s: expected JSON object at top level","messagePattern":"(.+?): expected JSON object at top level","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"openapiv3-merge/internal/merge/merge.go","lineNumber":188,"sourceCode":"// fields from extras. The token-based parser is used instead of\n// json.Unmarshal because we need to capture first-occurrence order of\n// unknown keys and the insertion order of `paths`/`webhooks` entries.\nfunc parse(in Input) (*document, error) {\n\td := &document{\n\t\tname:       in.Name,\n\t\tPaths:      newOrderedObject(),\n\t\tWebhooks:   newOrderedObject(),\n\t\tComponents: &components{},\n\t\textras:     newOrderedObject(),\n\t}\n\tdec := json.NewDecoder(bytes.NewReader(in.Data))\n\tdec.UseNumber()\n\ttok, err := dec.Token()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: %w\", in.Name, err)\n\t}\n\tif delim, ok := tok.(json.Delim); !ok || delim != '{' {\n\t\treturn nil, fmt.Errorf(\"%s: expected JSON object at top level\", in.Name)\n\t}\n\tfor dec.More() {\n\t\ttok, err := dec.Token()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%s: %w\", in.Name, err)\n\t\t}\n\t\tkey, ok := tok.(string)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s: unexpected token %v\", in.Name, tok)\n\t\t}\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%s: %q: %w\", in.Name, key, err)\n\t\t}\n\t\tswitch key {\n\t\tcase \"openapi\":\n\t\t\tif err := json.Unmarshal(raw, &d.OpenAPI); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"%s: openapi: %w\", in.Name, err)","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/grpc-ecosystem/grpc-gateway/blob/a58a4436a376a4bcc7d8f10c4d4f919a8438bba9/openapiv3-merge/internal/merge/merge.go#L170-L206","documentation":"This error is thrown when the first JSON token of an input document is not the '{' object-opening delimiter (merge.go:187-188). openapiv3-merge requires every merged document to be a JSON object at its root, because it iterates top-level keys (openapi, info, paths, components, ...) token by token. The input's JSON may be syntactically valid but its root is an array, string, number, or literal.","triggerScenarios":"Calling Merge with an Input whose Data is valid JSON but whose top-level value is e.g. an array [ ... ], a bare string, a number, or a YAML multi-document stream that decodes to a sequence — anything where dec.Token() returns a non-'{' Delim or a scalar token.","commonSituations":"Accidentally passing a JSON array of specs (e.g. output of jq -s .) instead of a single spec; a spec file whose root was replaced by a list during a refactor; passing a components-only fragment file that is an object but extracted paths array; concatenating files incorrectly.","solutions":["Ensure each Input's Data has a single JSON object ('{') as its root; peek at the first non-whitespace character before calling Merge.","If you have an array of specs, wrap or merge them into one object yourself, or pass each element as a separate merge.Input.","If the file is a YAML spec whose root is a mapping, convert it to JSON with a YAML-to-JSON converter that preserves the mapping root.","Validate the root kind with a quick decoder check: read one token and assert it is json.Delim('{') before calling Merge."],"exampleFix":"// before\nmerged, err := merge.Merge([]merge.Input{{Name: \"all\", Data: specsArrayJSON}})\n// after (specsArrayJSON is [ {...}, {...} ])\nvar specs []map[string]any\njson.Unmarshal(specsArrayJSON, &specs)\nmerged, err := merge.Merge([]merge.Input{\n    {Name: \"a\", Data: mustMarshal(specs[0])},\n    {Name: \"b\", Data: mustMarshal(specs[1])},\n})","handlingStrategy":"validation","validationCode":"func ensureRootIsObject(data []byte) error {\n    var probe any\n    dec := json.NewDecoder(bytes.NewReader(data))\n    dec.UseNumber()\n    tok, err := dec.Token()\n    if err != nil {\n        return fmt.Errorf(\"not valid JSON: %w\", err)\n    }\n    if d, ok := tok.(json.Delim); !ok || d != '{' {\n        return fmt.Errorf(\"top-level JSON value must be an object, got %v\", tok)\n    }\n    _ = probe\n    return nil\n}","typeGuard":"func hasObjectRoot(data []byte) bool {\n    tok, err := json.NewDecoder(bytes.NewReader(data)).Token()\n    if err != nil { return false }\n    d, ok := tok.(json.Delim)\n    return ok && d == '{'\n}","tryCatchPattern":null,"preventionTips":["Never pass JSON arrays or scalar values as Merge inputs; one spec object per Input.","If you have a list of specs, unpack the array and pass each element as its own Input.","Check the first non-whitespace byte of spec files in CI ('{' expected).","Avoid jq -s / text concatenation of specs; use Merge itself to combine documents."],"tags":["json","validation","openapi","go"],"backgroundTag":"json-root-not-object","analyzedSha":"a58a4436a376a4bcc7d8f10c4d4f919a8438bba9","analyzedAt":"2026-09-02T10:28:31.537Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T16:17:10.729Z"}