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

  1. Edit the input document so `openapi` is a quoted JSON string, e.g. "openapi": "3.1.0".
  2. 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).
  3. 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.
  4. 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

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.