Repository: grpc-ecosystem/grpc-gateway

Issue:
Automatically annotate using x-go-type
## 🚀 Feature

In a way I'm a bit surprised this hasn't been done / hasn't been requested.

Given the content of the proto file, it would be quite easy to generate the `x-go-type` annotation automatically for the OpenAPIv2 spec file, such that go-swagger can re-use the go type and not regenerate a new one. We use a monorepo, so for us this is super powerful.

I've managed to get it going using

```protobuf
option go_package = "<path/to/my/package>";

message <TypeName> {
   option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
        json_schema: {
          extensions: {
            key: "x-go-type"
            value: {
                struct_value: { 
                    fields: {
                        key: "type",
                        value: { string_value: "<TypeName>" }
                    },
                    fields: {
                        key: "import",
                        value: { 
                            struct_value: {
                                fields: {
                                    key: "package",
                                    value: { string_value: "<path/to/my/package>" }
                                } 
                            }
                        }
                    }
                }
            }
          }
        }
      }
}
```

So I guess I'm curious as to whether this was considered / rejected, and if PRs would be welcome here.

We wouldn't want to pollute JsonSchema of course, so I'd suggest this could go through another annotation.


Hints from issue comments:
@johanbrandhorst: Hi Norman, thanks for your issue! I don't remember this having been requested before. I think it could make sense as a feature to save users having to supply these annotations themselves everywhere, but it should be behind a generator option, so it doesn't add annotations to users that don't want it. Would you be interested in helping implement this?@NPellet: A generator option would be everything or nothing, though... Oh well I guess it would be enough

But the problem I will hit while doing so is that go-swagger expects the type to implement `Validate(formats strfmt.Registry) error` (instead of interface-checking at runtime), while `[protoc-gen-validate](https://github.com/bufbuild/protoc-gen-validate)` provides `Validate() error` 

Ah that's a bit annoying. Not directly related to this issue though. I'll see if I can get go-swagger to assert on the interface instead@rohitlohar45: Hi @johanbrandhorst ,  

I’d like to contribute to implementing support for `x-go-type` in the OpenAPI generator. To ensure alignment with the project's requirements, could you help clarify the following?  

1. Regarding the generator option approach you mentioned—should this be a global flag, or would it make sense to support more granular control (e.g., per-message or per-package)?  
2. Are there any specific concerns about how this might interact with other OpenAPI annotations or existing features?  

Additionally, is this feature currently implemented, or does it still need to be worked on? If it's available for contribution, I’d love to help implement it.  

Looking forward to your guidance!  

Thanks!@johanbrandhorst: Hi Rohit, it's great to hear that you'd like to work on implementing this feature. To answer your questions:
1. It should be a global flag, similar to our other generator options. Perhaps something like `generate_x_go_type`.
2. I think this should be orthogonal to our other options, but it will interact with the global `go_package` option, assuming that we want to generate an `x-go-type` annotation matching the generated Go protobuf type. 

Some more information for you: It seems like the `x-go-type` annotation is defined by this example: https://github.com/go-swagger/go-swagger/blob/0477d4a3af47ac446ab3d5d357ab36c03c117b93/fixtures/codegen/existing-model.yml#L99-L103. We'd probably need to support `import.package` and `type`. I don't think we should need to support `alias`. The `go_package` option should be used as the canonical import path for the generated types (you might be able to set `import.package` directly to the package value of this option).

Let me know if you need anymore information!@rohitlohar45: Thanks @johanbrandhorst for the detailed clarification! Let me summarize my understanding before proceeding:

1. We'll implement this as a global generator option named `generate_x_go_type`
2. The annotation will use the format from go-swagger's example:
   ```yaml
   x-go-type:
     import:
       package: <go_package_value>
     type: <message_name>
3. The go_package option from the proto file will be used directly as the import.package value
4. We won't implement the alias field as it's not needed for this use case@rohitlohar45: @johanbrandhorst can you check this PR https://github.com/grpc-ecosystem/grpc-gateway/pull/5340

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index becc70a2..0bc9565e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,7 +28,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-      - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
+      - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
         with:
           node-version: 10
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5
diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml
index 8152c7e8..e2ec2276 100644
--- a/.github/workflows/devcontainer.yml
+++ b/.github/workflows/devcontainer.yml
@@ -14,7 +14,7 @@ jobs:
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
       - name: Login to GitHub Container Registry
-        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
+        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
         with:
           registry: ghcr.io
           username: ${{ github.repository_owner }}
diff --git a/MODULE.bazel b/MODULE.bazel
index 11e14fe0..919c3c00 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -14,13 +14,13 @@ bazel_dep(name = "rules_go", version = "0.53.0", repo_name = "io_bazel_rules_go"
 bazel_dep(name = "rules_shell", version = "0.4.0")
 bazel_dep(name = "gazelle", version = "0.42.0", repo_name = "bazel_gazelle")
 bazel_dep(name = "bazel_skylib", version = "1.7.1")
-bazel_dep(name = "protobuf", version = "30.0", repo_name = "com_google_protobuf")
+bazel_dep(name = "protobuf", version = "30.1", repo_name = "com_google_protobuf")
 bazel_dep(name = "googleapis", version = "0.0.0-20241220-5e258e33.bcr.1")
 
 # This is required as a transitive dependency and not directly needed by this module.
 # We have this version pinned to solve for differences in the MODULE.bazel.lock file
 # when running CI.
-bazel_dep(name = "rules_rust", version = "0.58.0")
+bazel_dep(name = "rules_rust", version = "0.59.1")
 
 go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
 go_sdk.download(version = "1.24.0")
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 6a85c4ec..57a69365 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -96,8 +96,8 @@
     "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
     "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
     "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
-    "https://bcr.bazel.build/modules/protobuf/30.0/MODULE.bazel": "0e736de5d52ad7824113f47e65256a26ee74b689ba859c5447a0663e5a075409",
-    "https://bcr.bazel.build/modules/protobuf/30.0/source.json": "e1b5307330ca4a2f4fea02fc70b727798d8aa133d3c40f0f72e44e263b93b158",
+    "https://bcr.bazel.build/modules/protobuf/30.1/MODULE.bazel": "293a47b398800e1efeed6e325f2809c5fab6c692a80384c507afd0ffca3b1bcf",
+    "https://bcr.bazel.build/modules/protobuf/30.1/source.json": "688ec8aa4fc080a36b48d7b5c22dd3190de2efe07e82f54ccabd2f5e5a59b27e",
     "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
     "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34",
     "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680",
@@ -187,8 +187,8 @@
     "https://bcr.bazel.build/modules/rules_python/1.2.0/MODULE.bazel": "5aeeb48b2a6c19d668b48adf2b8a2b209a6310c230db0ce77450f148a89846e4",
     "https://bcr.bazel.build/modules/rules_python/1.2.0/source.json": "5b7892685c9a843526fd5a31e7d7a93eb819c59fd7b7fc444b5b143558e1b073",
     "https://bcr.bazel.build/modules/rules_rust/0.51.0/MODULE.bazel": "2b6d1617ac8503bfdcc0e4520c20539d4bba3a691100bee01afe193ceb0310f9",
-    "https://bcr.bazel.build/modules/rules_rust/0.58.0/MODULE.bazel": "3c8f4147982822c7d1fa63aecb1468c38ab9178107770df18031211a16719247",
-    "https://bcr.bazel.build/modules/rules_rust/0.58.0/source.json": "36262a3cdbd52eb89f275aa41877f0ea77aa4759c26ff76c6bfeb03aeff7b3dd",
+    "https://bcr.bazel.build/modules/rules_rust/0.59.1/MODULE.bazel": "b3203b59399fa01cf5844f42bb2a483f0db2ee11471805ad6855cdec4f5979e4",
+    "https://bcr.bazel.build/modules/rules_rust/0.59.1/source.json": "649a2d4b33e3f87ffc5d647d246ac0f1b33ea50be44f6d0265e5daffe7dc1dc8",
     "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
     "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
     "https://bcr.bazel.build/modules/rules_shell/0.4.0/MODULE.bazel": "0f8f11bb3cd11755f0b48c1de0bbcf62b4b34421023aa41a2fc74ef68d9584f0",
diff --git a/WORKSPACE b/WORKSPACE
index 0a778f4f..fa69130f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -35,17 +35,17 @@ http_archive(
 # Define before rules_proto, otherwise we receive the version of com_google_protobuf from there
 http_archive(
     name = "com_google_protobuf",
-    sha256 = "9df0e9e8ebe39f4fbbb9cf7db3d811287fe3616b2f191eb2bf5eaa12539c881f",
-    strip_prefix = "protobuf-30.0",
-    urls = ["https://github.com/protocolbuffers/protobuf/archive/v30.0.tar.gz"],
+    sha256 = "1451b03faec83aed17cdc71671d1bbdfd72e54086b827f5f6fd02bf7a4041b68",
+    strip_prefix = "protobuf-30.1",
+    urls = ["https://github.com/protocolbuffers/protobuf/archive/v30.1.tar.gz"],
 )
 
 http_archive(
     name = "googleapis",
-    sha256 = "e8e43dd32e538ff772c46effb7c7833db680da235b2eaf2acc0a72480638a8ff",
-    strip_prefix = "googleapis-e6136c12212ca0c8b4a8867a87cf51a3decf6e53",
+    sha256 = "3cca09e7d2d5620d684f1e20b5dc97f46b2648ef49f525cd6e38009bf9d3cd51",
+    strip_prefix = "googleapis-6155d42e99f45ebf53100d492852be1561916137",
     urls = [
-        "https://github.com/googleapis/googleapis/archive/e6136c12212ca0c8b4a8867a87cf51a3decf6e53.zip",
+        "https://github.com/googleapis/googleapis/archive/6155d42e99f45ebf53100d492852be1561916137.zip",
     ],
 )
 
diff --git a/examples/internal/clients/unannotatedecho/api/swagger.yaml b/examples/internal/clients/unannotatedecho/api/swagger.yaml
index 8cbe6190..60a38d87 100644
--- a/examples/internal/clients/unannotatedecho/api/swagger.yaml
+++ b/examples/internal/clients/unannotatedecho/api/swagger.yaml
@@ -448,6 +448,10 @@ definitions:
       note:
         type: "string"
     description: "Embedded represents a message embedded in SimpleMessage."
+    x-go-type:
+      import:
+        package: "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"
+      type: "UnannotatedEmbedded"
   examplepbUnannotatedNestedMessage:
     type: "object"
     properties:
@@ -455,6 +459,10 @@ definitions:
         type: "string"
       val:
         type: "string"
+    x-go-type:
+      import:
+        package: "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"
+      type: "UnannotatedNestedMessage"
   examplepbUnannotatedSimpleMessage:
     type: "object"
     required:
@@ -500,6 +508,10 @@ definitions:
       '@type':
         type: "string"
     additionalProperties: {}
+    x-go-type:
+      import:
+        package: "google.golang.org/protobuf/types/known/anypb"
+      type: "Any"
   rpcStatus:
     type: "object"
     properties:
@@ -512,6 +524,10 @@ definitions:
         type: "array"
         items:
           $ref: "#/definitions/protobufAny"
+    x-go-type:
+      import:
+        package: "google.golang.org/genproto/googleapis/rpc/status"
+      type: "Status"
 externalDocs:
   description: "More about gRPC-Gateway"
   url: "https://github.com/grpc-ecosystem/grpc-gateway"
diff --git a/examples/internal/proto/examplepb/unannotated_echo_service.buf.gen.yaml b/examples/internal/proto/examplepb/unannotated_echo_service.buf.gen.yaml
index 55a2c183..eac0412b 100644
--- a/examples/internal/proto/examplepb/unannotated_echo_service.buf.gen.yaml
+++ b/examples/internal/proto/examplepb/unannotated_echo_service.buf.gen.yaml
@@ -10,3 +10,4 @@ plugins:
     opt:
       - grpc_api_configuration=examples/internal/proto/examplepb/unannotated_echo_service.yaml
       - openapi_configuration=examples/internal/proto/examplepb/unannotated_echo_service.swagger.yaml
+      - generate_x_go_type=true
diff --git a/examples/internal/proto/examplepb/unannotated_echo_service.swagger.json b/examples/internal/proto/examplepb/unannotated_echo_service.swagger.json
index 6a497a0a..dcefd371 100644
--- a/examples/internal/proto/examplepb/unannotated_echo_service.swagger.json
+++ b/examples/internal/proto/examplepb/unannotated_echo_service.swagger.json
@@ -502,7 +502,13 @@
           "type": "string"
         }
       },
-      "description": "Embedded represents a message embedded in SimpleMessage."
+      "description": "Embedded represents a message embedded in SimpleMessage.",
+      "x-go-type": {
+        "import": {
+          "package": "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"
+        },
+        "type": "UnannotatedEmbedded"
+      }
     },
     "examplepbUnannotatedNestedMessage": {
       "type": "object",
@@ -513,6 +519,12 @@
         "val": {
           "type": "string"
         }
+      },
+      "x-go-type": {
+        "import": {
+          "package": "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"
+        },
+        "type": "UnannotatedNestedMessage"
       }
     },
     "examplepbUnannotatedSimpleMessage": {
@@ -576,7 +588,13 @@
           "type": "string"
         }
       },
-      "additionalProperties": {}
+      "additionalProperties": {},
+      "x-go-type": {
+        "import": {
+          "package": "google.golang.org/protobuf/types/known/anypb"
+        },
+        "type": "Any"
+      }
     },
     "rpcStatus": {
       "type": "object",
@@ -595,6 +613,12 @@
             "$ref": "#/definitions/protobufAny"
           }
         }
+      },
+      "x-go-type": {
+        "import": {
+          "package": "google.golang.org/genproto/googleapis/rpc/status"
+        },
+        "type": "Status"
       }
     }
   },
diff --git a/go.mod b/go.mod
index f663bd79..79e68c91 100644
--- a/go.mod
+++ b/go.mod
@@ -8,8 +8,8 @@ require (
 	github.com/rogpeppe/fastuuid v1.2.0
 	golang.org/x/oauth2 v0.28.0
 	golang.org/x/text v0.23.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf
+	google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4
 	google.golang.org/grpc v1.71.0
 	google.golang.org/protobuf v1.36.5
 	gopkg.in/yaml.v3 v3.0.1
@@ -17,7 +17,7 @@ require (
 
 require (
 	github.com/kr/pretty v0.3.1 // indirect
-	golang.org/x/net v0.35.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
+	golang.org/x/net v0.37.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 )
diff --git a/go.sum b/go.sum
index 1d8a266f..80b52aed 100644
--- a/go.sum
+++ b/go.sum
@@ -35,18 +35,18 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce
 go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
 go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
 go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
 golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf h1:BdIVRm+fyDUn8lrZLPSlBCfM/YKDwUBYgDoLv9+DYo0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
 google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
 google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
diff --git a/internal/descriptor/registry.go b/internal/descriptor/registry.go
index 9ee910b9..743cea8f 100644
--- a/internal/descriptor/registry.go
+++ b/internal/descriptor/registry.go
@@ -178,6 +178,9 @@ type Registry struct {
 	// This leads to more compliant and readable OpenAPI suitable for documentation, but may complicate client
 	// implementation if you want to pass the original "name" parameter.
 	expandSlashedPathPatterns bool
+
+	// generateXGoType is a global generator option for generating x-go-type annotations
+	generateXGoType bool
 }
 
 type repeatedFieldSeparator struct {
@@ -919,3 +922,11 @@ func (r *Registry) SetExpandSlashedPathPatterns(expandSlashedPathPatterns bool)
 func (r *Registry) GetExpandSlashedPathPatterns() bool {
 	return r.expandSlashedPathPatterns
 }
+
+func (r *Registry) SetGenerateXGoType(generateXGoType bool) {
+	r.generateXGoType = generateXGoType
+}
+
+func (r *Registry) GetGenerateXGoType() bool {
+	return r.generateXGoType
+}
diff --git a/protoc-gen-openapiv2/defs.bzl b/protoc-gen-openapiv2/defs.bzl
index 661ae473..29e548c0 100644
--- a/protoc-gen-openapiv2/defs.bzl
+++ b/protoc-gen-openapiv2/defs.bzl
@@ -77,7 +77,8 @@ def _run_proto_gen_openapi(
         disable_default_responses,
         enable_rpc_deprecation,
         expand_slashed_path_patterns,
-        preserve_rpc_order):
+        preserve_rpc_order,
+        generate_x_go_type):
     args = actions.args()
 
     args.add("--plugin", "protoc-gen-openapiv2=%s" % protoc_gen_openapiv2.path)
@@ -159,6 +160,8 @@ def _run_proto_gen_openapi(
 
     if preserve_rpc_order:
         args.add("--openapiv2_opt", "preserve_rpc_order=true")
+    if generate_x_go_type:
+        args.add("--openapiv2_opt", "generate_x_go_type=true")
 
     args.add("--openapiv2_opt", "repeated_path_param_separator=%s" % repeated_path_param_separator)
 
@@ -270,6 +273,7 @@ def _proto_gen_openapi_impl(ctx):
                     enable_rpc_deprecation = ctx.attr.enable_rpc_deprecation,
                     expand_slashed_path_patterns = ctx.attr.expand_slashed_path_patterns,
                     preserve_rpc_order = ctx.attr.preserve_rpc_order,
+                    generate_x_go_type = ctx.attr.generate_x_go_type,
                 ),
             ),
         ),
@@ -450,6 +454,11 @@ protoc_gen_openapiv2 = rule(
             doc = "if set, uses proto3 field semantics for the OpenAPI schema." +
                   "  This means that fields are required by default.",
         ),
+        "generate_x_go_type": attr.bool(
+            default = False,
+            mandatory = False,
+            doc = "Generate x-go-type extension using the go_package option from proto files",
+        ),
         "_protoc": attr.label(
             default = "@com_google_protobuf//:protoc",
             executable = True,
diff --git a/protoc-gen-openapiv2/internal/genopenapi/template.go b/protoc-gen-openapiv2/internal/genopenapi/template.go
index 0feec148..2da26d38 100644
--- a/protoc-gen-openapiv2/internal/genopenapi/template.go
+++ b/protoc-gen-openapiv2/internal/genopenapi/template.go
@@ -2,6 +2,7 @@ package genopenapi
 
 import (
 	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"math"
@@ -517,6 +518,25 @@ func renderMessageAsDefinition(msg *descriptor.Message, reg *descriptor.Registry
 			Type: "object",
 		},
 	}
+
+	if reg.GetGenerateXGoType() && msg.File.GoPkg.Path != "" {
+		if schema.extensions == nil {
+			schema.extensions = []extension{}
+		}
+		goTypeName := msg.GetName()
+
+		goTypeName = casing.JSONCamelCase(goTypeName)
+		schema.extensions = append(schema.extensions, extension{
+			key: "x-go-type",
+			value: json.RawMessage(`{
+                "import": {
+                    "package": "` + msg.File.GoPkg.Path + `"
+                },
+                "type": "` + goTypeName + `"
+            }`),
+		})
+	}
+
 	msgComments := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index))
 	if err := updateOpenAPIDataFromComments(reg, &schema, msg, msgComments, false); err != nil {
 		return openapiSchemaObject{}, err
diff --git a/protoc-gen-openapiv2/main.go b/protoc-gen-openapiv2/main.go
index 09dc38cd..82062cc9 100644
--- a/protoc-gen-openapiv2/main.go
+++ b/protoc-gen-openapiv2/main.go
@@ -52,6 +52,7 @@ var (
 	enableRpcDeprecation           = flag.Bool("enable_rpc_deprecation", false, "whether to process grpc method's deprecated option.")
 	expandSlashedPathPatterns      = flag.Bool("expand_slashed_path_patterns", false, "if set, expands path parameters with URI sub-paths into the URI. For example, \"/v1/{name=projects/*}/resource\" becomes \"/v1/projects/{project}/resource\".")
 	useProto3FieldSemantics        = flag.Bool("use_proto3_field_semantics", false, "if set, uses proto3 field semantics for the OpenAPI schema. This means that fields are required by default.")
+	generateXGoType                = flag.Bool("generate_x_go_type", false, "if set, generates x-go-type extension using the go_package option from proto files")
 
 	_ = flag.Bool("logtostderr", false, "Legacy glog compatibility. This flag is a no-op, you can safely remove it")
 )
@@ -177,6 +178,7 @@ func main() {
 	reg.SetPreserveRPCOrder(*preserveRPCOrder)
 	reg.SetEnableRpcDeprecation(*enableRpcDeprecation)
 	reg.SetExpandSlashedPathPatterns(*expandSlashedPathPatterns)
+	reg.SetGenerateXGoType(*generateXGoType)
 
 	if err := reg.SetRepeatedPathParamSeparator(*repeatedPathParamSeparator); err != nil {
 		emitError(err)
diff --git a/repositories.bzl b/repositories.bzl
index 178ab455..475b2938 100644
--- a/repositories.bzl
+++ b/repositories.bzl
@@ -1875,14 +1875,14 @@ def go_repositories():
     go_repository(
         name = "org_golang_google_genproto_googleapis_api",
         importpath = "google.golang.org/genproto/googleapis/api",
-        sum = "h1:BdIVRm+fyDUn8lrZLPSlBCfM/YKDwUBYgDoLv9+DYo0=",
-        version = "v0.0.0-20250311190419-81fb87f6b8bf",
+        sum = "h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=",
+        version = "v0.0.0-20250313205543-e70fdf4c4cb4",
     )
     go_repository(
         name = "org_golang_google_genproto_googleapis_rpc",
         importpath = "google.golang.org/genproto/googleapis/rpc",
-        sum = "h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=",
-        version = "v0.0.0-20250311190419-81fb87f6b8bf",
+        sum = "h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=",
+        version = "v0.0.0-20250313205543-e70fdf4c4cb4",
     )
     go_repository(
         name = "org_golang_google_grpc",
@@ -1906,8 +1906,8 @@ def go_repositories():
     go_repository(
         name = "org_golang_x_crypto",
         importpath = "golang.org/x/crypto",
-        sum = "h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=",
-        version = "v0.33.0",
+        sum = "h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=",
+        version = "v0.36.0",
     )
     go_repository(
         name = "org_golang_x_exp",
@@ -1944,8 +1944,8 @@ def go_repositories():
     go_repository(
         name = "org_golang_x_net",
         importpath = "golang.org/x/net",
-        sum = "h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=",
-        version = "v0.35.0",
+        sum = "h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=",
+        version = "v0.37.0",
     )
     go_repository(
         name = "org_golang_x_oauth2",
@@ -1962,14 +1962,14 @@ def go_repositories():
     go_repository(
         name = "org_golang_x_sys",
         importpath = "golang.org/x/sys",
-        sum = "h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=",
-        version = "v0.30.0",
+        sum = "h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=",
+        version = "v0.31.0",
     )
     go_repository(
         name = "org_golang_x_term",
         importpath = "golang.org/x/term",
-        sum = "h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=",
-        version = "v0.29.0",
+        sum = "h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=",
+        version = "v0.30.0",
     )
     go_repository(
         name = "org_golang_x_text",



Test patch:
diff --git a/protoc-gen-openapiv2/internal/genopenapi/generator_test.go b/protoc-gen-openapiv2/internal/genopenapi/generator_test.go
index 70af1944..eb4456d9 100644
--- a/protoc-gen-openapiv2/internal/genopenapi/generator_test.go
+++ b/protoc-gen-openapiv2/internal/genopenapi/generator_test.go
@@ -1,6 +1,7 @@
 package genopenapi_test
 
 import (
+	"bytes"
 	"os"
 	"reflect"
 	"sort"
@@ -1854,3 +1855,86 @@ func TestFindExpectedPaths(t *testing.T) {
 		})
 	}
 }
+
+func TestGenerateXGoType(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name           string
+		inputProtoText string
+		wantYAML       string
+	}{
+		{
+			name:           "x-go-type extension",
+			inputProtoText: "testdata/generator/x_go_type.prototext",
+			wantYAML:       "testdata/generator/x_go_type.swagger.yaml",
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			b, err := os.ReadFile(tt.inputProtoText)
+			if err != nil {
+				t.Fatal(err)
+			}
+			var req pluginpb.CodeGeneratorRequest
+			if err := prototext.Unmarshal(b, &req); err != nil {
+				t.Fatal(err)
+			}
+
+			reg := descriptor.NewRegistry()
+			reg.SetGenerateXGoType(true)
+			if err := reg.Load(&req); err != nil {
+				t.Fatalf("failed to load request: %s", err)
+			}
+
+			var targets []*descriptor.File
+			for _, target := range req.FileToGenerate {
+				f, err := reg.LookupFile(target)
+				if err != nil {
+					t.Fatalf("failed to lookup file: %s", err)
+				}
+				targets = append(targets, f)
+			}
+
+			g := genopenapi.New(reg, genopenapi.FormatYAML)
+			resp, err := g.Generate(targets)
+			if err != nil {
+				t.Fatalf("failed to generate: %s", err)
+			}
+
+			if len(resp) != 1 {
+				t.Fatalf("expected 1 file, got %d", len(resp))
+			}
+
+			want, err := os.ReadFile(tt.wantYAML)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			var gotMap, wantMap map[string]interface{}
+			if err := yaml.Unmarshal([]byte(resp[0].GetContent()), &gotMap); err != nil {
+				t.Fatalf("failed to unmarshal generated YAML: %v", err)
+			}
+			if err := yaml.Unmarshal(want, &wantMap); err != nil {
+				t.Fatalf("failed to unmarshal expected YAML: %v", err)
+			}
+
+			gotYAML, err := yaml.Marshal(gotMap)
+			if err != nil {
+				t.Fatalf("failed to marshal got YAML: %v", err)
+			}
+			wantYAML, err := yaml.Marshal(wantMap)
+			if err != nil {
+				t.Fatalf("failed to marshal want YAML: %v", err)
+			}
+
+			if !bytes.Equal(gotYAML, wantYAML) {
+				t.Errorf("YAMLs don't match:\ngot:\n%s\nwant:\n%s", gotYAML, wantYAML)
+			}
+		})
+	}
+}
diff --git a/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/x_go_type.prototext b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/x_go_type.prototext
new file mode 100644
index 00000000..42ed06f5
--- /dev/null
+++ b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/x_go_type.prototext
@@ -0,0 +1,32 @@
+file_to_generate: "test/service/v1/service.proto"
+proto_file: {
+  name: "test/service/v1/service.proto"
+  package: "test.service.v1"
+  message_type: {
+    name: "TestMessage"
+    field: {
+      name: "value"
+      number: 1
+      label: LABEL_OPTIONAL
+      type: TYPE_STRING
+      json_name: "value"
+    }
+  }
+  service: {
+    name: "TestService"
+    method: {
+      name: "Test"
+      input_type: ".test.service.v1.TestMessage"
+      output_type: ".test.service.v1.TestMessage"
+      options: {
+        [google.api.http]: {
+          post: "/v1/test"
+          body: "*"
+        }
+      }
+    }
+  }
+  options: {
+    go_package: "github.com/grpc-ecosystem/grpc-gateway/v2/test/service/v1;servicev1"
+  }
+}
\ No newline at end of file
diff --git a/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/x_go_type.swagger.yaml b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/x_go_type.swagger.yaml
new file mode 100644
index 00000000..e4230eb1
--- /dev/null
+++ b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/x_go_type.swagger.yaml
@@ -0,0 +1,37 @@
+swagger: "2.0"
+info:
+  title: test/service/v1/service.proto
+  version: version not set
+tags:
+- name: TestService
+consumes:
+- application/json
+produces:
+- application/json
+paths:
+  /v1/test:
+    post:
+      operationId: TestService_Test
+      responses:
+        "200":
+          description: A successful response.
+          schema:
+            $ref: '#/definitions/v1TestMessage'
+      parameters:
+      - name: body
+        in: body
+        required: true
+        schema:
+          $ref: '#/definitions/v1TestMessage'
+      tags:
+      - TestService
+definitions:
+  v1TestMessage:
+    type: object
+    properties:
+      value:
+        type: string
+    x-go-type:
+      import:
+        package: "github.com/grpc-ecosystem/grpc-gateway/v2/test/service/v1"
+      type: "TestMessage"
\ No newline at end of file