grpc-ecosystem/grpc-gateway · error

security: %s declares different requirements

Error message

security: %s declares different requirements

What it means

openapiv3-merge applies first-wins to the root `security` array, but only when later inputs agree. If a later input declares a non-empty `security` that differs (after canonical comparison) from the one already accumulated, the merge fails rather than silently changing which authentication the combined API requires.

Source

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

	}
	if len(out.Security) == 0 {
		out.Security = src.Security
		return nil
	}
	a, err := json.Marshal(out.Security)
	if err != nil {
		return fmt.Errorf("security: %w", err)
	}
	b, err := json.Marshal(src.Security)
	if err != nil {
		return fmt.Errorf("security: %w", err)
	}
	same, err := canonicalEqual(a, b)
	if err != nil {
		return fmt.Errorf("security: %w", err)
	}
	if !same {
		return fmt.Errorf("security: %s declares different requirements", src.name)
	}
	return nil
}

// mergeExtras applies first-wins to unknown top-level keys (notably
// extensions `x-*`). Conflicting redeclarations from later inputs are
// silently ignored, matching the policy for info/servers/etc.
func mergeExtras(dst, src *orderedObject) {
	for _, k := range src.keys {
		if _, ok := dst.get(k); ok {
			continue
		}
		dst.set(k, src.vals[k])
	}
}

// canonicalEqual reports whether a and b decode to the same JSON value
// under canonical (sorted-key) encoding.

View on GitHub (pinned to a58a4436a3)

Solutions

  1. Make the root `security` arrays identical (canonically) across all input files
  2. Remove the root `security` from all but one input so the first declaration wins
  3. Split the merge into documents that genuinely share one auth model, or keep them separate
  4. If alternatives should be unioned intentionally, preprocess the specs to agree on the combined list first

Example fix

// before: a.json {"security":[{"apiKey":[]}]}, b.json {"security":[{"oauth2":["read"]}]}
// after: b.json {"security":[{"apiKey":[]}]} (or delete b.json's security field)
Defensive patterns

Strategy: validation

Validate before calling

func checkSecurityConflict(files []string) error {
    var first json.RawMessage
    for _, f := range files {
        b, _ := os.ReadFile(f)
        var doc struct {
            Security json.RawMessage `json:"security"`
        }
        if err := json.Unmarshal(b, &doc); err != nil { return err }
        if len(doc.Security) == 0 || string(doc.Security) == "null" { continue }
        norm := normalizeJSON(doc.Security) // jq-style canonical form
        if first == nil {
            first = norm
        } else if norm != first {
            return fmt.Errorf("%s declares different security requirements", f)
        }
    }
    return nil
}

Type guard

func hasSecurity(doc map[string]json.RawMessage) bool {
    sec, ok := doc["security"]
    return ok && len(sec) > 0 && string(sec) != "null"
}

Try / catch

if err := merge.Merge(inputs); err != nil {
    if strings.Contains(err.Error(), "declares different requirements") {
        // prompt user to unify security or drop it from conflicting inputs
    }
    return err
}

Prevention

When it happens

Trigger: Running `openapiv3-merge a.json b.json` where a.json has root security e.g. [{"apiKey":[]}] and b.json has different non-empty security e.g. [{"oauth2":["read"]}].

Common situations: Two services merged into one gateway spec but each declaring different auth schemes; one spec regenerated with an added or changed security requirement; a spec adding a second alternative requirement to an existing list.

Related errors


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