diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 698fbc5c9..13d359b6e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -17,6 +17,9 @@ jobs:
       run: |
         python -m pip install --upgrade setuptools pip wheel
         python -m pip install nox
+    - name: Install pytest
+      run: |
+        python -m pip install pytest
     - name: Run docs
       run: |
         nox -s docs
@@ -33,6 +36,9 @@ jobs:
       run: |
         python -m pip install --upgrade setuptools pip wheel
         python -m pip install nox
+    - name: Install pytest
+      run: |
+        python -m pip install pytest
     - name: Run docfx
       run: |
         nox -s docfx
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 1051da0bd..29499a8b9 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,6 +17,9 @@ jobs:
       run: |
         python -m pip install --upgrade setuptools pip wheel
         python -m pip install nox
+    - name: Install pytest
+      run: |
+        python -m pip install pytest
     - name: Run lint
       run: |
         nox -s lint
diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
index e6a79291d..85f2db3ee 100644
--- a/.github/workflows/mypy.yml
+++ b/.github/workflows/mypy.yml
@@ -17,6 +17,9 @@ jobs:
       run: |
         python -m pip install --upgrade setuptools pip wheel
         python -m pip install nox
+    - name: Install pytest
+      run: |
+        python -m pip install pytest
     - name: Run mypy
       run: |
         nox -s mypy
diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml
index 2cfaada36..20ccd9c36 100644
--- a/.github/workflows/unittest.yml
+++ b/.github/workflows/unittest.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        option: ["", "_grpc_gcp", "_wo_grpc"]
+        option: ["", "_grpc_gcp", "_wo_grpc", "_prerelease"]
         python:
           - "3.7"
           - "3.8"
@@ -26,6 +26,16 @@ jobs:
             python: 3.8
           - option: "_wo_grpc"
             python: 3.9
+          - option: "_prerelease"
+            python: 3.7
+          - option: "_prerelease"
+            python: 3.8
+          - option: "_prerelease"
+            python: 3.9
+          - option: "_prerelease"
+            python: 3.10
+          - option: "_prerelease"
+            python: 3.11
     steps:
     - name: Checkout
       uses: actions/checkout@v4
@@ -37,6 +47,9 @@ jobs:
       run: |
         python -m pip install --upgrade setuptools pip wheel
         python -m pip install nox
+    - name: Install pytest
+      run: |
+        python -m pip install pytest
     - name: Run unit tests
       env:
         COVERAGE_FILE: .coverage${{ matrix.option }}-${{matrix.python }}
diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py
index 49f99d210..038704657 100644
--- a/google/api_core/operations_v1/transports/rest.py
+++ b/google/api_core/operations_v1/transports/rest.py
@@ -29,9 +29,13 @@
 from google.longrunning import operations_pb2  # type: ignore
 from google.protobuf import empty_pb2  # type: ignore
 from google.protobuf import json_format  # type: ignore
+import google.protobuf
+
 import grpc
 from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport
 
+PROTOBUF_VERSION = google.protobuf.__version__
+
 OptionalRetry = Union[retries.Retry, object]
 
 DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(
@@ -184,11 +188,22 @@ def _list_operations(
                 "google.longrunning.Operations.ListOperations"
             ]
 
-        request_kwargs = json_format.MessageToDict(
-            request,
-            preserving_proto_field_name=True,
-            including_default_value_fields=True,
-        )
+        # For backwards compatibility with protobuf 3.x 4.x
+        # Remove once support for protobuf 3.x and 4.x is dropped
+        # https://github.com/googleapis/python-api-core/issues/643
+        if PROTOBUF_VERSION[0:2] in ["3.", "4."]:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                including_default_value_fields=True,  # type: ignore # backward compatibility
+            )
+        else:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                always_print_fields_with_no_presence=True,
+            )
+
         transcoded_request = path_template.transcode(http_options, **request_kwargs)
 
         uri = transcoded_request["uri"]
@@ -199,7 +214,6 @@ def _list_operations(
         json_format.ParseDict(transcoded_request["query_params"], query_params_request)
         query_params = json_format.MessageToDict(
             query_params_request,
-            including_default_value_fields=False,
             preserving_proto_field_name=False,
             use_integers_for_enums=False,
         )
@@ -265,11 +279,22 @@ def _get_operation(
                 "google.longrunning.Operations.GetOperation"
             ]
 
-        request_kwargs = json_format.MessageToDict(
-            request,
-            preserving_proto_field_name=True,
-            including_default_value_fields=True,
-        )
+        # For backwards compatibility with protobuf 3.x 4.x
+        # Remove once support for protobuf 3.x and 4.x is dropped
+        # https://github.com/googleapis/python-api-core/issues/643
+        if PROTOBUF_VERSION[0:2] in ["3.", "4."]:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                including_default_value_fields=True,  # type: ignore # backward compatibility
+            )
+        else:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                always_print_fields_with_no_presence=True,
+            )
+
         transcoded_request = path_template.transcode(http_options, **request_kwargs)
 
         uri = transcoded_request["uri"]
@@ -280,7 +305,6 @@ def _get_operation(
         json_format.ParseDict(transcoded_request["query_params"], query_params_request)
         query_params = json_format.MessageToDict(
             query_params_request,
-            including_default_value_fields=False,
             preserving_proto_field_name=False,
             use_integers_for_enums=False,
         )
@@ -339,11 +363,21 @@ def _delete_operation(
                 "google.longrunning.Operations.DeleteOperation"
             ]
 
-        request_kwargs = json_format.MessageToDict(
-            request,
-            preserving_proto_field_name=True,
-            including_default_value_fields=True,
-        )
+        # For backwards compatibility with protobuf 3.x 4.x
+        # Remove once support for protobuf 3.x and 4.x is dropped
+        # https://github.com/googleapis/python-api-core/issues/643
+        if PROTOBUF_VERSION[0:2] in ["3.", "4."]:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                including_default_value_fields=True,  # type: ignore # backward compatibility
+            )
+        else:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                always_print_fields_with_no_presence=True,
+            )
         transcoded_request = path_template.transcode(http_options, **request_kwargs)
 
         uri = transcoded_request["uri"]
@@ -354,7 +388,6 @@ def _delete_operation(
         json_format.ParseDict(transcoded_request["query_params"], query_params_request)
         query_params = json_format.MessageToDict(
             query_params_request,
-            including_default_value_fields=False,
             preserving_proto_field_name=False,
             use_integers_for_enums=False,
         )
@@ -411,11 +444,21 @@ def _cancel_operation(
                 "google.longrunning.Operations.CancelOperation"
             ]
 
-        request_kwargs = json_format.MessageToDict(
-            request,
-            preserving_proto_field_name=True,
-            including_default_value_fields=True,
-        )
+        # For backwards compatibility with protobuf 3.x 4.x
+        # Remove once support for protobuf 3.x and 4.x is dropped
+        # https://github.com/googleapis/python-api-core/issues/643
+        if PROTOBUF_VERSION[0:2] in ["3.", "4."]:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                including_default_value_fields=True,  # type: ignore # backward compatibility
+            )
+        else:
+            request_kwargs = json_format.MessageToDict(
+                request,
+                preserving_proto_field_name=True,
+                always_print_fields_with_no_presence=True,
+            )
         transcoded_request = path_template.transcode(http_options, **request_kwargs)
 
         # Jsonify the request body
@@ -423,7 +466,6 @@ def _cancel_operation(
         json_format.ParseDict(transcoded_request["body"], body_request)
         body = json_format.MessageToDict(
             body_request,
-            including_default_value_fields=False,
             preserving_proto_field_name=False,
             use_integers_for_enums=False,
         )
@@ -435,7 +477,6 @@ def _cancel_operation(
         json_format.ParseDict(transcoded_request["query_params"], query_params_request)
         query_params = json_format.MessageToDict(
             query_params_request,
-            including_default_value_fields=False,
             preserving_proto_field_name=False,
             use_integers_for_enums=False,
         )
diff --git a/noxfile.py b/noxfile.py
index 2c7ec6c72..da4c34ce3 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -15,6 +15,8 @@
 from __future__ import absolute_import
 import os
 import pathlib
+import pytest
+import re
 import shutil
 
 # https://github.com/google/importlab/issues/25
@@ -26,6 +28,8 @@
 # Black and flake8 clash on the syntax for ignoring flake8's F401 in this file.
 BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"]
 
+PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+
 DEFAULT_PYTHON_VERSION = "3.10"
 CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
 
@@ -72,7 +76,37 @@ def blacken(session):
     session.run("black", *BLACK_EXCLUDES, *BLACK_PATHS)
 
 
-def default(session, install_grpc=True):
+def install_prerelease_dependencies(session, constraints_path):
+    with open(constraints_path, encoding="utf-8") as constraints_file:
+        constraints_text = constraints_file.read()
+        # Ignore leading whitespace and comment lines.
+        constraints_deps = [
+            match.group(1)
+            for match in re.finditer(
+                r"^\s*(\S+)(?===\S+)", constraints_text, flags=re.MULTILINE
+            )
+        ]
+        session.install(*constraints_deps)
+        prerel_deps = [
+            "google-auth",
+            "googleapis-common-protos",
+            "grpcio",
+            "grpcio-status",
+            "proto-plus",
+            "protobuf",
+        ]
+
+        for dep in prerel_deps:
+            session.install("--pre", "--no-deps", "--upgrade", dep)
+
+        # Remaining dependencies
+        other_deps = [
+            "requests",
+        ]
+        session.install(*other_deps)
+
+
+def default(session, install_grpc=True, prerelease=False):
     """Default unit test session.
 
     This is intended to be run **without** an interpreter set, so
@@ -80,9 +114,8 @@ def default(session, install_grpc=True):
     Python corresponding to the ``nox`` binary the ``PATH`` can
     run the tests.
     """
-    constraints_path = str(
-        CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
-    )
+    if prerelease and not install_grpc:
+        pytest.skip("The pre-release session cannot be run without grpc")
 
     session.install(
         "dataclasses",
@@ -92,10 +125,37 @@ def default(session, install_grpc=True):
         "pytest-xdist",
     )
 
-    if install_grpc:
-        session.install("-e", ".[grpc]", "-c", constraints_path)
+    constraints_dir = str(CURRENT_DIRECTORY / "testing")
+
+    if prerelease:
+        install_prerelease_dependencies(
+            session, f"{constraints_dir}/constraints-{PYTHON_VERSIONS[0]}.txt"
+        )
+        # This *must* be the last install command to get the package from source.
+        session.install("-e", ".", "--no-deps")
     else:
-        session.install("-e", ".", "-c", constraints_path)
+        if install_grpc:
+            session.install(
+                "-e",
+                ".[grpc]",
+                "-c",
+                f"{constraints_dir}/constraints-{session.python}.txt",
+            )
+        else:
+            session.install(
+                "-e", ".", "-c", f"{constraints_dir}/constraints-{session.python}.txt"
+            )
+
+    # Print out package versions of dependencies
+    session.run(
+        "python", "-c", "import google.protobuf; print(google.protobuf.__version__)"
+    )
+    # Support for proto.version was added in v1.23.0
+    # https://github.com/googleapis/proto-plus-python/releases/tag/v1.23.0
+    session.run("python", "-c", """import proto; hasattr(proto, "version") and print(proto.version.__version__)""")
+    if install_grpc:
+        session.run("python", "-c", "import grpc; print(grpc.__version__)")
+    session.run("python", "-c", "import google.auth; print(google.auth.__version__)")
 
     pytest_args = [
         "python",
@@ -130,15 +190,26 @@ def default(session, install_grpc=True):
     session.run(*pytest_args)
 
 
-@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"])
+@nox.session(python=PYTHON_VERSIONS)
 def unit(session):
     """Run the unit test suite."""
     default(session)
 
 
-@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"])
+@nox.session(python=PYTHON_VERSIONS[-1])
+def unit_prerelease(session):
+    """Run the unit test suite."""
+    default(session, prerelease=True)
+
+
+@nox.session(python=PYTHON_VERSIONS)
 def unit_grpc_gcp(session):
-    """Run the unit test suite with grpcio-gcp installed."""
+    """
+    Run the unit test suite with grpcio-gcp installed.
+    `grpcio-gcp` doesn't support protobuf 4+.
+    Remove extra `grpcgcp` when protobuf 3.x is dropped.
+    https://github.com/googleapis/python-api-core/issues/594
+    """
     constraints_path = str(
         CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
     )
@@ -150,7 +221,7 @@ def unit_grpc_gcp(session):
     default(session)
 
 
-@nox.session(python=["3.8", "3.10", "3.11", "3.12"])
+@nox.session(python=PYTHON_VERSIONS)
 def unit_wo_grpc(session):
     """Run the unit test suite w/o grpcio installed"""
     default(session, install_grpc=False)
@@ -164,10 +235,10 @@ def lint_setup_py(session):
     session.run("python", "setup.py", "check", "--restructuredtext", "--strict")
 
 
-@nox.session(python="3.8")
+@nox.session(python=DEFAULT_PYTHON_VERSION)
 def pytype(session):
     """Run type-checking."""
-    session.install(".[grpc]", "pytype >= 2019.3.21")
+    session.install(".[grpc]", "pytype")
     session.run("pytype")
 
 
@@ -175,12 +246,10 @@ def pytype(session):
 def mypy(session):
     """Run type-checking."""
     session.install(".[grpc]", "mypy")
-    # Exclude types-protobuf==4.24.0.20240106
-    # See https://github.com/python/typeshed/issues/11254
     session.install(
         "types-setuptools",
         "types-requests",
-        "types-protobuf!=4.24.0.20240106",
+        "types-protobuf",
         "types-mock",
         "types-dataclasses",
     )
diff --git a/setup.py b/setup.py
index 47e0b454c..a9e01f49c 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@
 release_status = "Development Status :: 5 - Production/Stable"
 dependencies = [
     "googleapis-common-protos >= 1.56.2, < 2.0.dev0",
-    "protobuf>=3.19.5,<5.0.0.dev0,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5",
+    "protobuf>=3.19.5,<6.0.0.dev0,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5",
     "proto-plus >= 1.22.3, <2.0.0dev",
     "google-auth >= 2.14.1, < 3.0.dev0",
     "requests >= 2.18.0, < 3.0.0.dev0",