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

  1. Ensure each Input's Data has a single JSON object ('{') as its root; peek at the first non-whitespace character before calling Merge.
  2. If you have an array of specs, wrap or merge them into one object yourself, or pass each element as a separate merge.Input.
  3. 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.
  4. 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

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.