grpc-ecosystem/grpc-gateway · error
: 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.
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.
Appendix: 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)