grpc-ecosystem/grpc-gateway · error

expected JSON object, got %v

Error message

expected JSON object, got %v

What it means

decodeOrderedObject expects the raw JSON it decodes to start with a `{` delimiter, i.e. be a JSON object. When the first token is anything else (array, string, number, true/false/null, or an unexpected token), it fails with `expected JSON object, got <token>`. The merge pipeline uses this to parse sections that must be objects while preserving key order.

Source

Thrown at openapiv3-merge/internal/merge/merge.go:560

	}
	buf.WriteByte('}')
	return buf.Bytes(), nil
}

// decodeOrderedObject parses a JSON object into an orderedObject, preserving
// the input's key order.
func decodeOrderedObject(raw json.RawMessage) (*orderedObject, error) {
	if isJSONNull(raw) {
		return newOrderedObject(), nil
	}
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.UseNumber()
	tok, err := dec.Token()
	if err != nil {
		return nil, err
	}
	if d, ok := tok.(json.Delim); !ok || d != '{' {
		return nil, fmt.Errorf("expected JSON object, got %v", tok)
	}
	out := newOrderedObject()
	for dec.More() {
		tok, err := dec.Token()
		if err != nil {
			return nil, err
		}
		k, ok := tok.(string)
		if !ok {
			return nil, fmt.Errorf("expected string key, got %v", tok)
		}
		var v json.RawMessage
		if err := dec.Decode(&v); err != nil {
			return nil, err
		}
		out.set(k, v)
	}
	if _, err := dec.Token(); err != nil {

View on GitHub (pinned to a58a4436a3)

Solutions

  1. Ensure every input file is a single JSON object starting with `{` (run `jq . file.json` to check and normalize)
  2. Convert YAML specs to JSON before merging (the tool parses JSON)
  3. Check for BOM characters or leading whitespace/comments that shift the first token; strip them
  4. Verify the file is not truncated or wrapped in an array by the producing tool

Example fix

// before: specs.json contains
[{"openapi":"3.0.0",...}]
// after: unwrap to a single object
{"openapi":"3.0.0",...}
Defensive patterns

Strategy: validation

Validate before calling

func ensureJSONObject(path string) error {
    b, err := os.ReadFile(path)
    if err != nil { return err }
    dec := json.NewDecoder(bytes.NewReader(bytes.TrimPrefix(b, []byte("\xef\xbb\xbf"))))
    tok, err := dec.Token()
    if err != nil { return err }
    if d, ok := tok.(json.Delim); !ok || d != '{' {
        return fmt.Errorf("%s: top-level value is not a JSON object", path)
    }
    return nil
}
// run for every input before merge.Merge

Type guard

func isJSONObject(b []byte) bool {
    dec := json.NewDecoder(bytes.NewReader(b))
    tok, err := dec.Token()
    if err != nil { return false }
    d, ok := tok.(json.Delim)
    return ok && d == '{'
}

Try / catch

if err := merge.Merge(inputs); err != nil {
    if strings.Contains(err.Error(), "expected JSON object") {
        return fmt.Errorf("an input file is not a JSON object; convert YAML or unwrap arrays first: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling parse on input whose raw data does not begin with a JSON object — e.g. the file contains a JSON array `[...]`, a bare string, `null` (non-raw-null path), `123`, or trailing garbage — so the first decoded token is not '{'.

Common situations: Pointing openapiv3-merge at a file that is a JSON array or YAML file instead of a JSON object; an empty or truncated file producing a different token; a wrapper script emitting an envelope object around the spec.

Related errors


AI-assisted analysis of grpc-ecosystem/grpc-gateway@a58a4436a3 (2026-09-02). Data as JSON: /api/errors/40aa41d748614975. Report an issue: GitHub.