grpc-ecosystem/grpc-gateway · error
%s: openapi: %w
Error message
%s: openapi: %w
What it means
This error is returned by the library's internal parse step when the top-level `openapi` field of an input document fails to json.Unmarshal into a string. The `openapi` field of an OpenAPI document must be a JSON string (e.g. "3.1.0"), so any other JSON type — number, object, array, bool, or null — causes Unmarshal to fail and the error is wrapped as `<name>: openapi: <cause>`. It is raised while merging inputs via Merge, before any merge logic runs.
Source
Thrown at openapiv3-merge/internal/merge/merge.go:206
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)
}
case "info":
d.Info = raw
case "servers":
d.Servers = raw
case "paths":
obj, err := decodeOrderedObject(raw)
if err != nil {
return nil, fmt.Errorf("%s: paths: %w", in.Name, err)
}
d.Paths = obj
case "webhooks":
obj, err := decodeOrderedObject(raw)
if err != nil {
return nil, fmt.Errorf("%s: webhooks: %w", in.Name, err)
}
d.Webhooks = obj
case "components":View on GitHub (pinned to a58a4436a3)
Solutions
- Edit the input document so `openapi` is a quoted JSON string, e.g. "openapi": "3.1.0".
- If the spec is YAML, ensure the conversion to JSON quotes the version (YAML parsers often turn 3.0.0 into a string, but 3.0 into a float).
- Validate the input JSON before calling Merge: json.Unmarshal the `openapi` value into a string in your own pre-check to get a clearer error.
- If generating specs in code, emit the version as a string literal, never a number or object.
Example fix
// before
{"openapi": 3.1, "info": {"title": "api"}}
// after
{"openapi": "3.1.0", "info": {"title": "api"}} Defensive patterns
Strategy: validation
Validate before calling
func validateOpenAPIField(data []byte) error {
var doc struct {
OpenAPI json.RawMessage `json:"openapi"`
}
if err := json.Unmarshal(data, &doc); err != nil {
return err
}
var v string
if err := json.Unmarshal(doc.OpenAPI, &v); err != nil {
return fmt.Errorf("openapi must be a JSON string: %w", err)
}
return nil
} Type guard
func isOpenAPIString(raw json.RawMessage) bool {
var v string
return json.Unmarshal(raw, &v) == nil
} Try / catch
doc, err := merger.Merge(inputs)
if err != nil {
var perr *parseError // or inspect the wrapped message
if strings.Contains(err.Error(), ": openapi: ") {
return fmt.Errorf("input spec has invalid openapi version field: %w", err)
}
return err
} Prevention
- Always quote the OpenAPI version in JSON/YAML output (openapi: "3.0.3").
- When converting YAML to JSON, force the openapi value to a string to avoid numeric coercion.
- Run your specs through a JSON Schema / OpenAPI validator before merging.
- Add a CI lint step that json.Unmarshals `openapi` into a string for every spec fed to the merger.
When it happens
Trigger: Calling Merge with an Input whose Data contains `"openapi": 3.0` (number instead of string), `"openapi": {"version": "3.0.0"}`, `"openapi": null`, or `"openapi": ["3.0.0"]` — any non-string JSON value for the `openapi` key.
Common situations: Hand-written or template-generated specs where the version was written unquoted (YAML `openapi: 3.0.0` converted carelessly to JSON); code that builds specs programmatically and assigns the version as a float; specs produced by tools that emit an object like `{"openapi": {"value": "3.0.3"}}`.
Related errors
AI-assisted analysis of grpc-ecosystem/grpc-gateway@a58a4436a3 (2026-09-02).
Data as JSON: /api/errors/975b3a5ffd5b80c3.
Report an issue: GitHub.