grpc-ecosystem/grpc-gateway · error
%s: expected JSON object at top level
Error message
%s: expected JSON object at top level
What it means
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.
Source
Thrown at openapiv3-merge/internal/merge/merge.go:188
// fields from extras. The token-based parser is used instead of
// json.Unmarshal because we need to capture first-occurrence order of
// unknown keys and the insertion order of `paths`/`webhooks` entries.
func parse(in Input) (*document, error) {
d := &document{
name: in.Name,
Paths: newOrderedObject(),
Webhooks: newOrderedObject(),
Components: &components{},
extras: newOrderedObject(),
}
dec := json.NewDecoder(bytes.NewReader(in.Data))
dec.UseNumber()
tok, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("%s: %w", in.Name, err)
}
if delim, ok := tok.(json.Delim); !ok || delim != '{' {
return nil, fmt.Errorf("%s: expected JSON object at top level", in.Name)
}
for dec.More() {
tok, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("%s: %w", in.Name, err)
}
key, ok := tok.(string)
if !ok {
return nil, fmt.Errorf("%s: unexpected token %v", in.Name, tok)
}
var raw json.RawMessage
if err := dec.Decode(&raw); err != nil {
return nil, fmt.Errorf("%s: %q: %w", in.Name, key, err)
}
switch key {
case "openapi":
if err := json.Unmarshal(raw, &d.OpenAPI); err != nil {
return nil, fmt.Errorf("%s: openapi: %w", in.Name, err)View on GitHub (pinned to a58a4436a3)
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.
Example fix
// before
merged, err := merge.Merge([]merge.Input{{Name: "all", Data: specsArrayJSON}})
// after (specsArrayJSON is [ {...}, {...} ])
var specs []map[string]any
json.Unmarshal(specsArrayJSON, &specs)
merged, err := merge.Merge([]merge.Input{
{Name: "a", Data: mustMarshal(specs[0])},
{Name: "b", Data: mustMarshal(specs[1])},
}) Defensive patterns
Strategy: validation
Validate before calling
func ensureRootIsObject(data []byte) error {
var probe any
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
tok, err := dec.Token()
if err != nil {
return fmt.Errorf("not valid JSON: %w", err)
}
if d, ok := tok.(json.Delim); !ok || d != '{' {
return fmt.Errorf("top-level JSON value must be an object, got %v", tok)
}
_ = probe
return nil
} Type guard
func hasObjectRoot(data []byte) bool {
tok, err := json.NewDecoder(bytes.NewReader(data)).Token()
if err != nil { return false }
d, ok := tok.(json.Delim)
return ok && d == '{'
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of grpc-ecosystem/grpc-gateway@a58a4436a3 (2026-09-02).
Data as JSON: /api/errors/469447b4307dbf73.
Report an issue: GitHub.