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
- Ensure every input file is a single JSON object starting with `{` (run `jq . file.json` to check and normalize)
- Convert YAML specs to JSON before merging (the tool parses JSON)
- Check for BOM characters or leading whitespace/comments that shift the first token; strip them
- 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
- Always feed openapiv3-merge JSON object files, never YAML or arrays
- Convert YAML specs with a yaml-to-json step in your pipeline
- Strip BOMs and trailing garbage when files pass through editors or scripts
- Sanity-check inputs with `jq . file.json` in CI before merging
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.