Skip to content

AIP-193: clarify ErrorInfo metadata rationale and typed detail guidance #1615

@yordis

Description

@yordis

Intent

I am trying to understand the design intent behind AIP-193 and google.rpc.ErrorInfo, not request a breaking change to the existing Google RPC error model.

The practical motivation is API documentation and code generation from proto definitions. When documenting failure contracts, it is useful to know which values are the canonical error identity, which values are metadata for that identity, and which values are independent typed error details.

I would like to understand why ErrorInfo.metadata was designed as map<string, string>, how typed domain metadata is expected to be represented, and whether AIP-193 has a preferred convention for avoiding duplicate sources of truth.

Current Model

AIP-193 makes google.rpc.ErrorInfo the canonical machine-readable identity for an API error. The (domain, reason) pair identifies the error, and ErrorInfo.metadata carries dynamic context for that error.

Today, that dynamic context is limited to map<string, string>. That works for simple scalar values such as IDs, names, zones, limits, and resource types, but it leaves API authors without a standard way to attach typed domain metadata to the canonical ErrorInfo.

google.rpc.Status.details can carry arbitrary protobuf messages through google.protobuf.Any, but those details are flat siblings. A custom typed detail can be filtered by its own type URL, but generic tooling cannot know that the custom detail is specifically the typed metadata payload for the required ErrorInfo.

This matters for API documentation and code generation. If failure contracts are generated from proto definitions, the generator can document the required ErrorInfo, and it can document additional detail types, but it does not have a standard role marker for "this detail is typed metadata for the ErrorInfo".

Current shape

AIP-193 supports this shape today:

{
  "code": 5,
  "message": "The requested user was not found.",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.ErrorInfo",
      "reason": "USER_NOT_FOUND",
      "domain": "users.acme.example",
      "metadata": {
        "user_id": "usr_123",
        "tenant_id": "tenant_456"
      }
    }
  ]
}

That is useful, but the typed metadata shape cannot be represented directly inside ErrorInfo.metadata.

An API can add a custom typed detail:

{
  "@type": "type.googleapis.com/acme.users.v1.UserNotFoundMetadata",
  "user_id": "usr_123",
  "tenant_id": "tenant_456"
}

However, as a flat sibling in Status.details, this does not carry a standard indication that it is metadata for google.rpc.ErrorInfo. It is only a domain-specific detail message.

Alternative Shape Considered

The shape that would have made the role explicit is a typed metadata companion detail under Status.details, alongside google.rpc.ErrorInfo.

For example:

message ErrorInfoMetadata {
  google.protobuf.Any metadata = 1;
}

The type name is illustrative. The important shape is a standard detail wrapper whose payload is typed metadata.

The wire shape would keep the existing ErrorInfo unchanged and add a sibling companion detail:

{
  "code": 5,
  "message": "The requested user was not found.",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.ErrorInfo",
      "reason": "USER_NOT_FOUND",
      "domain": "users.acme.example",
      "metadata": {
        "user_id": "usr_123",
        "tenant_id": "tenant_456"
      }
    },
    {
      "@type": "type.googleapis.com/google.rpc.ErrorInfoMetadata",
      "metadata": {
        "@type": "type.googleapis.com/acme.users.v1.UserNotFoundMetadata",
        "user_id": "usr_123",
        "tenant_id": "tenant_456"
      }
    }
  ]
}

The companion detail intentionally does not repeat domain or reason. It is scoped by being in the same Status.details list as the required google.rpc.ErrorInfo; repeating the identity fields would create two places for the error identity to drift.

This avoids changing google.rpc.ErrorInfo, so existing clients can continue to rely on the required ErrorInfo payload. Clients and tools that understand the new companion detail can unpack the typed metadata.

The important part is the role marker:

type.googleapis.com/google.rpc.ErrorInfoMetadata

That type tells generic tooling that the enclosed Any is typed metadata for the canonical ErrorInfo in the same Status.details list.

I am not assuming this companion detail should be added. I am using it to make the question concrete: was a shape like this intentionally avoided, and if so, what tradeoff led AIP-193 toward string metadata plus independent typed details instead?

Why not only use custom typed details?

A custom typed detail can work for bespoke clients, but it does not solve the generic tooling problem.

For example, a tool can see this:

{
  "@type": "type.googleapis.com/acme.users.v1.UserNotFoundMetadata",
  "user_id": "usr_123",
  "tenant_id": "tenant_456"
}

But it cannot know, without API-specific convention, whether this is:

  • typed metadata for ErrorInfo;
  • a separate domain error detail;
  • user-facing remediation data;
  • debugging data;
  • or some other independent detail payload.

A standard companion detail would let tools filter by the standard wrapper type first, then unpack the domain-specific metadata type inside.

Duplication Concern

If APIs use both ErrorInfo.metadata and a typed detail for the same data, there is a risk of creating two sources of truth.

For example, user_id and tenant_id could appear in:

  • ErrorInfo.metadata;
  • a custom typed detail such as acme.users.v1.UserNotFoundMetadata;
  • or both.

It is not clear whether AIP-193 expects one of those to be canonical, or whether duplication is expected for compatibility and message interpolation.

A possible convention would be:

  • use ErrorInfo.metadata for simple string context and values referenced by human-readable messages;
  • use typed details for richer structured data;
  • avoid mirroring the entire typed payload into ErrorInfo.metadata.

If that is the intended model, it would be helpful to document it explicitly.

Why not change ErrorInfo.metadata?

Changing ErrorInfo.metadata from map<string, string> to google.protobuf.Any would express the model directly, but it would be a breaking change.

The companion detail above is one possible additive shape that keeps the existing contract intact:

  • google.rpc.ErrorInfo remains required and unchanged.
  • ErrorInfo.metadata remains available for simple string context.
  • Unknown clients can ignore the companion detail.
  • New clients can unpack typed metadata when present.

Again, I am not asking for that specific type to be added. I am asking whether AIP-193 intentionally prefers another convention, and why.

Questions

  • What was the rationale for making ErrorInfo.metadata a map<string, string> instead of a typed payload?
  • Should APIs avoid JSON-encoding structured payloads into ErrorInfo.metadata values?
  • Should APIs use custom typed details directly when metadata needs structure, even though those details are flat siblings and do not carry a standard "metadata for ErrorInfo" role?
  • If the same values can appear in both ErrorInfo.metadata and a typed detail, should one be considered canonical?
  • Should one representation be dropped when the other is present, except for values required by AIP-193 for message interpolation?
  • Was a companion detail shape such as ErrorInfoMetadata { google.protobuf.Any metadata = 1; } considered and intentionally avoided?
  • What convention should API authors and documentation generators use to associate typed metadata with the required ErrorInfo?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions