Skip to content

Mark deprecated services and methods in generated code #382

Description

@stefanvanburen

protoc-gen-connectrpc ignores option deprecated = true on services and methods, so nothing in the generated code signals to callers that an RPC is on its way out.

The other Connect implementations surface it through each language's standard marker, which editors and linters pick up:

  • connect-go writes // Deprecated: do not use. on the FooServiceClient and FooServiceHandler interfaces for a deprecated service, and on the interface methods for a deprecated method (main.go#L652-L753).
  • connect-es gets its service stubs from protoc-gen-es, which adds @deprecated to the JSDoc of a deprecated rpc, and of a service when the service or its file is deprecated (jsdoc.ts#L71-L85).
  • protobuf-py, which we build on, appends [deprecated = true] to the proto provenance line in message, enum, and field docstrings, with no decorator (protoc_gen_py/__init__.py#L497-L586). It doesn't generate services, and its DescService and DescMethod already expose deprecated.

I propose applying PEP 702's deprecated decorator, imported from typing_extensions (already a runtime dependency), with category=None so there's no runtime warning, matching Go and JS. Neither of those uses the proto comment as the deprecation reason, and the comment already lands in the docstring, so the message is a fixed string naming the element:

from typing_extensions import deprecated

@deprecated("foo.v1.FooService is deprecated.", category=None)
class FooServiceClient(ConnectClient):
    @deprecated("foo.v1.FooService.Bar is deprecated.", category=None)
    async def bar(self, request: BarRequest, ...) -> BarResponse: ...

For a deprecated service, the decorator goes on the client classes and the service Protocols (async and sync). For a deprecated method, it goes on the corresponding client and Protocol methods. Type checkers then report constructing the client and calling deprecated methods, whether through the client or the Protocol; implementing the Protocol isn't flagged. ty reports these by default, while pyright needs reportDeprecated (on in strict mode) and mypy needs enable_error_code = ["deprecated"].

The alternative is to follow protobuf-py and only note deprecation in docstrings. That keeps the two generators consistent but is invisible to type checkers, which is most of the point of the option. If the decorator approach lands here, it may be worth proposing the same for messages and fields in protobuf-py.

Open question: should category=None be the only behavior, or should a plugin option opt into a runtime DeprecationWarning?

Activity

  1. anuraaga commented on Oct 1, 2026

    @anuraaga
    Collaborator

    Thanks for bringing this up, I didn't realize the decorator is available from typing_extensions. It's tricky at least in protobuf-py, where normal fields are very messy to handle (constructor overloads, replacing attribute with @property / @property.setter to be able to apply a decorator) or seemingly impossible for enum values, extensions, oneof fields.

    So we would need to decide if something partial is ok / how much we would want consistency

    • Whether to use @deprecated here even if protobuf-py doesn't
    • Whether to use @deprecated on what's easy only across both repos, types and RPC methods
    • Add the messiness for fields at least. Accept that coverage still isn't complete

    In my experience, fields are commonly deprecated. Getting a notice in type checkers would be quite nice indeed so perhaps there is bang for buck in supporting them. I think enum values are also relatively commonly deprecated though and the inconsistency feels unfortunate. And I am not excited to only mark message / enums deprecated and not fields.

    I am currently leaning towards not annotating, both here and in protobuf-py, treating them as one shared cosystem, and not wanting to annotate in a half-baked way. In which case I would suggest going with the text form. But what do you think?

    For example, this is something like what handling fields would look like (with more than one deprecated field, it just gets worse)

    class Msg(Message[_MsgFields]):
        if not TYPE_CHECKING:
            __slots__ = ("value", "old")
    
        if TYPE_CHECKING:
            @overload
            def __init__(self, *, value: int = 0) -> None: ...
            @overload
            @deprecated("pkg.Msg.old is deprecated.", category=None)
            def __init__(self, *, value: int = 0, old: int = 0) -> None: ...
            def __init__(self, *, value: int = 0, old: int = 0) -> None: ...
    
            value: int
    
            @property
            @deprecated("pkg.Msg.old is deprecated.", category=None)
            def old(self) -> int: ...
            @old.setter
            @deprecated("pkg.Msg.old is deprecated.", category=None)
            def old(self, value: int) -> None: ...
  2. stefanvanburen commented on Oct 1, 2026

    @stefanvanburen
    MemberAuthor

    yeah, I don't love the inconsistency with protobuf-py. Since most of the difficulty is upstream, it might be worth opening an issue there and noodling a little more, leaving this open in the meantime? Like you said, I don't think it's trivial (or perhaps, even possible) to support the decorator on all of the protobuf constructs supported by protobuf-py.

    FWIW, it looks like mypy-protobuf only supports a subset of protobuf deprecated options as well.

  3. anuraaga commented on Oct 2, 2026

    @anuraaga
    Collaborator

    Ended up warming to it, and it will be good to do it here too (probably after the next protobuf-py release for the suppression, which I think we need too for servers) - bufbuild/protobuf-py#123

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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