diff --git a/mojo/public/cpp/bindings/lib/connector.cc b/mojo/public/cpp/bindings/lib/connector.cc
index 8343acbfc79f2..db72df0dba069 100644
--- a/mojo/public/cpp/bindings/lib/connector.cc
+++ b/mojo/public/cpp/bindings/lib/connector.cc
@@ -430,11 +430,29 @@ bool Connector::ReadSingleMessage(MojoResult* read_result) {
   // during message dispatch.
   base::WeakPtr<Connector> weak_self = weak_self_;
 
-  Message message;
-  const MojoResult rv = ReadMessage(message_pipe_.get(), &message);
+  ScopedMessageHandle message_handle;
+  const MojoResult rv = ReadMessageNew(message_pipe_.get(), &message_handle,
+                                       MOJO_READ_MESSAGE_FLAG_NONE);
   *read_result = rv;
 
   if (rv == MOJO_RESULT_OK) {
+    Message message = Message::CreateFromMessageHandle(&message_handle);
+    if (message.IsNull()) {
+      // Even if the read was successful, the Message may still be null if there
+      // was a problem extracting handles from it. We treat this essentially as
+      // a bad IPC because we don't really have a better option.
+      //
+      // We include |heap_profiler_tag_| in the error message since it usually
+      // (via this Connector's owner) provides useful information about which
+      // binding interface is using this Connector.
+      NotifyBadMessage(message_handle.get(),
+                       std::string(heap_profiler_tag_) +
+                           "One or more handle attachments were invalid.");
+      HandleError(false /* force_pipe_reset */,
+                  false /* force_async_handler */);
+      return false;
+    }
+
     base::Optional<ActiveDispatchTracker> dispatch_tracker;
     if (!is_dispatching_ && nesting_observer_) {
       is_dispatching_ = true;
diff --git a/mojo/public/cpp/bindings/lib/message.cc b/mojo/public/cpp/bindings/lib/message.cc
index 8972d9efd1d98..b9070b76966f8 100644
--- a/mojo/public/cpp/bindings/lib/message.cc
+++ b/mojo/public/cpp/bindings/lib/message.cc
@@ -157,7 +157,7 @@ void DestroyUnserializedContext(uintptr_t context) {
   delete reinterpret_cast<internal::UnserializedMessageContext*>(context);
 }
 
-ScopedMessageHandle CreateUnserializedMessageObject(
+Message CreateUnserializedMessage(
     std::unique_ptr<internal::UnserializedMessageContext> context) {
   ScopedMessageHandle handle;
   MojoResult rv = mojo::CreateMessage(&handle);
@@ -168,7 +168,8 @@ ScopedMessageHandle CreateUnserializedMessageObject(
       handle->value(), reinterpret_cast<uintptr_t>(context.release()),
       &SerializeUnserializedContext, &DestroyUnserializedContext, nullptr);
   DCHECK_EQ(MOJO_RESULT_OK, rv);
-  return handle;
+
+  return Message::CreateFromMessageHandle(&handle);
 }
 
 }  // namespace
@@ -192,7 +193,7 @@ Message::Message(Message&& other)
 }
 
 Message::Message(std::unique_ptr<internal::UnserializedMessageContext> context)
-    : Message(CreateUnserializedMessageObject(std::move(context))) {}
+    : Message(CreateUnserializedMessage(std::move(context))) {}
 
 Message::Message(uint32_t name,
                  uint32_t flags,
@@ -206,53 +207,53 @@ Message::Message(uint32_t name,
   serialized_ = true;
 }
 
-Message::Message(ScopedMessageHandle handle) {
+// static
+Message Message::CreateFromMessageHandle(ScopedMessageHandle* message_handle) {
+  DCHECK(message_handle);
+  const MessageHandle& handle = message_handle->get();
   DCHECK(handle.is_valid());
 
   uintptr_t context_value = 0;
   MojoResult get_context_result =
-      MojoGetMessageContext(handle->value(), nullptr, &context_value);
+      MojoGetMessageContext(handle.value(), nullptr, &context_value);
   if (get_context_result == MOJO_RESULT_NOT_FOUND) {
     // It's a serialized message. Extract handles if possible.
     uint32_t num_bytes;
     void* buffer;
     uint32_t num_handles = 0;
-    MojoResult rv = MojoGetMessageData(handle->value(), nullptr, &buffer,
+    std::vector<ScopedHandle> handles;
+    MojoResult rv = MojoGetMessageData(handle.value(), nullptr, &buffer,
                                        &num_bytes, nullptr, &num_handles);
     if (rv == MOJO_RESULT_RESOURCE_EXHAUSTED) {
-      handles_.resize(num_handles);
-      rv = MojoGetMessageData(handle->value(), nullptr, &buffer, &num_bytes,
-                              reinterpret_cast<MojoHandle*>(handles_.data()),
+      handles.resize(num_handles);
+      rv = MojoGetMessageData(handle.value(), nullptr, &buffer, &num_bytes,
+                              reinterpret_cast<MojoHandle*>(handles.data()),
                               &num_handles);
-    } else {
-      // No handles, so it's safe to retransmit this message if the caller
-      // really wants to.
-      transferable_ = true;
     }
 
     if (rv != MOJO_RESULT_OK) {
-      // Failed to deserialize handles. Leave the Message uninitialized.
-      return;
+      // Failed to deserialize handles. Return a null message and leave the
+      // |*message_handle| intact.
+      return Message();
     }
 
-    payload_buffer_ = internal::Buffer(buffer, num_bytes, num_bytes);
-    serialized_ = true;
-  } else {
-    DCHECK_EQ(MOJO_RESULT_OK, get_context_result);
-    auto* context =
-        reinterpret_cast<internal::UnserializedMessageContext*>(context_value);
-    // Dummy data address so common header accessors still behave properly. The
-    // choice is V1 reflects unserialized message capabilities: we may or may
-    // not need to support request IDs (which require at least V1), but we never
-    // (for now, anyway) need to support associated interface handles (V2).
-    payload_buffer_ =
-        internal::Buffer(context->header(), sizeof(internal::MessageHeaderV1),
-                         sizeof(internal::MessageHeaderV1));
-    transferable_ = true;
-    serialized_ = false;
+    return Message(std::move(*message_handle), std::move(handles),
+                   internal::Buffer(buffer, num_bytes, num_bytes),
+                   true /* serialized */);
   }
 
-  handle_ = std::move(handle);
+  DCHECK_EQ(MOJO_RESULT_OK, get_context_result);
+  auto* context =
+      reinterpret_cast<internal::UnserializedMessageContext*>(context_value);
+  // Dummy data address so common header accessors still behave properly. The
+  // choice is V1 reflects unserialized message capabilities: we may or may
+  // not need to support request IDs (which require at least V1), but we never
+  // (for now, anyway) need to support associated interface handles (V2).
+  internal::Buffer payload_buffer(context->header(),
+                                  sizeof(internal::MessageHeaderV1),
+                                  sizeof(internal::MessageHeaderV1));
+  return Message(std::move(*message_handle), {}, std::move(payload_buffer),
+                 false /* serialized */);
 }
 
 Message::~Message() = default;
@@ -430,7 +431,8 @@ void Message::SerializeIfNecessary() {
     return;
 
   // Reconstruct this Message instance from the serialized message's handle.
-  *this = Message(std::move(handle_));
+  ScopedMessageHandle handle = std::move(handle_);
+  *this = CreateFromMessageHandle(&handle);
 }
 
 std::unique_ptr<internal::UnserializedMessageContext>
@@ -455,6 +457,16 @@ Message::TakeUnserializedContext(
   return base::WrapUnique(context);
 }
 
+Message::Message(ScopedMessageHandle message_handle,
+                 std::vector<ScopedHandle> attached_handles,
+                 internal::Buffer payload_buffer,
+                 bool serialized)
+    : handle_(std::move(message_handle)),
+      payload_buffer_(std::move(payload_buffer)),
+      handles_(std::move(attached_handles)),
+      transferable_(!serialized || handles_.empty()),
+      serialized_(serialized) {}
+
 bool MessageReceiver::PrefersSerializedMessages() {
   return false;
 }
@@ -491,17 +503,6 @@ ReportBadMessageCallback SyncMessageResponseContext::GetBadMessageCallback() {
   return base::BindOnce(&DoNotifyBadMessage, std::move(response_));
 }
 
-MojoResult ReadMessage(MessagePipeHandle handle, Message* message) {
-  ScopedMessageHandle message_handle;
-  MojoResult rv =
-      ReadMessageNew(handle, &message_handle, MOJO_READ_MESSAGE_FLAG_NONE);
-  if (rv != MOJO_RESULT_OK)
-    return rv;
-
-  *message = Message(std::move(message_handle));
-  return MOJO_RESULT_OK;
-}
-
 void ReportBadMessage(const std::string& error) {
   internal::MessageDispatchContext* context =
       internal::MessageDispatchContext::current();
diff --git a/mojo/public/cpp/bindings/message.h b/mojo/public/cpp/bindings/message.h
index 7f6e3ea436cea..791cb53bc66f6 100644
--- a/mojo/public/cpp/bindings/message.h
+++ b/mojo/public/cpp/bindings/message.h
@@ -74,7 +74,11 @@ class COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE) Message {
   // If the message had any handles attached, they will be extracted and
   // retrievable via |handles()|. Such messages may NOT be sent back over
   // another message pipe, but are otherwise safe to inspect and pass around.
-  Message(ScopedMessageHandle handle);
+  //
+  // If handles are attached and their extraction fails for any reason,
+  // |*handle| remains unchanged and the returned Message will be null (i.e.
+  // calling IsNull() on it will return |true|).
+  static Message CreateFromMessageHandle(ScopedMessageHandle* message_handle);
 
   ~Message();
 
@@ -90,6 +94,11 @@ class COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE) Message {
   // Indicates whether this Message is uninitialized.
   bool IsNull() const { return !handle_.is_valid(); }
 
+  // Indicates whether this Message is in valid state. A Message may be in an
+  // invalid state iff it failed partial deserialization during construction
+  // over a ScopedMessageHandle.
+  bool IsValid() const;
+
   // Indicates whether this Message is serialized.
   bool is_serialized() const { return serialized_; }
 
@@ -222,6 +231,14 @@ class COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE) Message {
 #endif
 
  private:
+  // Internal constructor used by |CreateFromMessageHandle()| when either there
+  // are no attached handles or all attached handles are successfully extracted
+  // from the message object.
+  Message(ScopedMessageHandle message_handle,
+          std::vector<ScopedHandle> attached_handles,
+          internal::Buffer payload_buffer,
+          bool serialized);
+
   ScopedMessageHandle handle_;
 
   // A Buffer which may be used to allocate blocks of data within the message
@@ -358,16 +375,6 @@ class COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE) SyncMessageResponseContext {
   DISALLOW_COPY_AND_ASSIGN(SyncMessageResponseContext);
 };
 
-// Read a single message from the pipe. The caller should have created the
-// Message, but not called Initialize(). Returns MOJO_RESULT_SHOULD_WAIT if
-// the caller should wait on the handle to become readable. Returns
-// MOJO_RESULT_OK if the message was read successfully and should be
-// dispatched, otherwise returns an error code if something went wrong.
-//
-// NOTE: The message hasn't been validated and may be malformed!
-COMPONENT_EXPORT(MOJO_CPP_BINDINGS_BASE)
-MojoResult ReadMessage(MessagePipeHandle handle, Message* message);
-
 // Reports the currently dispatching Message as bad. Note that this is only
 // legal to call from directly within the stack frame of a message dispatch. If
 // you need to do asynchronous work before you can determine the legitimacy of
diff --git a/mojo/public/cpp/bindings/tests/validation_unittest.cc b/mojo/public/cpp/bindings/tests/validation_unittest.cc
index 748e6428c374b..d1d6c996a0d25 100644
--- a/mojo/public/cpp/bindings/tests/validation_unittest.cc
+++ b/mojo/public/cpp/bindings/tests/validation_unittest.cc
@@ -51,7 +51,7 @@ Message CreateRawMessage(size_t size) {
                              nullptr, 0, &options, &buffer, &buffer_size);
   DCHECK_EQ(MOJO_RESULT_OK, rv);
 
-  return Message(std::move(handle));
+  return Message::CreateFromMessageHandle(&handle);
 }
 
 template <typename T>
diff --git a/mojo/public/cpp/test_support/test_utils.h b/mojo/public/cpp/test_support/test_utils.h
index d56ca3fe3362a..68d20f5aea91b 100644
--- a/mojo/public/cpp/test_support/test_utils.h
+++ b/mojo/public/cpp/test_support/test_utils.h
@@ -21,7 +21,9 @@ bool SerializeAndDeserialize(UserType* input, UserType* output) {
   // This accurately simulates full serialization to ensure that all attached
   // handles are serialized as well. Necessary for DeserializeFromMessage to
   // work properly.
-  message = mojo::Message(message.TakeMojoMessage());
+  mojo::ScopedMessageHandle handle = message.TakeMojoMessage();
+  message = mojo::Message::CreateFromMessageHandle(&handle);
+  DCHECK(!message.IsNull());
 
   return MojomType::DeserializeFromMessage(std::move(message), output);
 }
diff --git a/services/viz/public/cpp/compositing/BUILD.gn b/services/viz/public/cpp/compositing/BUILD.gn
index 555d5bed7fb69..4f61f0ad06573 100644
--- a/services/viz/public/cpp/compositing/BUILD.gn
+++ b/services/viz/public/cpp/compositing/BUILD.gn
@@ -15,6 +15,7 @@ source_set("tests") {
     "//components/viz/test:test_support",
     "//gpu/ipc/common:struct_traits",
     "//media/capture/mojom:video_capture",
+    "//mojo/public/cpp/test_support:test_utils",
     "//services/service_manager/public/cpp",
     "//services/service_manager/public/cpp:service_test_support",
     "//services/viz/public/interfaces",
diff --git a/services/viz/public/cpp/compositing/struct_traits_unittest.cc b/services/viz/public/cpp/compositing/struct_traits_unittest.cc
index cedae73a57595..0993a63c0d6d9 100644
--- a/services/viz/public/cpp/compositing/struct_traits_unittest.cc
+++ b/services/viz/public/cpp/compositing/struct_traits_unittest.cc
@@ -25,6 +25,7 @@
 #include "ipc/ipc_message_utils.h"
 #include "mojo/public/cpp/base/time_mojom_traits.h"
 #include "mojo/public/cpp/bindings/binding_set.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "services/viz/public/cpp/compositing/begin_frame_args_struct_traits.h"
 #include "services/viz/public/cpp/compositing/compositor_frame_metadata_struct_traits.h"
 #include "services/viz/public/cpp/compositing/compositor_frame_struct_traits.h"
@@ -67,24 +68,6 @@ namespace {
 
 using StructTraitsTest = testing::Test;
 
-// Test StructTrait serialization and deserialization for copyable type. |input|
-// will be serialized and then deserialized into |output|.
-template <class MojomType, class Type>
-void SerializeAndDeserialize(const Type& input, Type* output) {
-  MojomType::DeserializeFromMessage(
-      mojo::Message(MojomType::SerializeAsMessage(&input).TakeMojoMessage()),
-      output);
-}
-
-// Test StructTrait serialization and deserialization for move only type.
-// |input| will be serialized and then deserialized into |output|.
-template <class MojomType, class Type>
-void SerializeAndDeserialize(Type&& input, Type* output) {
-  MojomType::DeserializeFromMessage(
-      mojo::Message(MojomType::SerializeAsMessage(&input).TakeMojoMessage()),
-      output);
-}
-
 }  // namespace
 
 TEST_F(StructTraitsTest, BeginFrameArgs) {
@@ -107,7 +90,7 @@ TEST_F(StructTraitsTest, BeginFrameArgs) {
   input.animate_only = animate_only;
 
   BeginFrameArgs output;
-  SerializeAndDeserialize<mojom::BeginFrameArgs>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::BeginFrameArgs>(&input, &output);
 
   EXPECT_EQ(source_id, output.source_id);
   EXPECT_EQ(sequence_number, output.sequence_number);
@@ -129,7 +112,7 @@ TEST_F(StructTraitsTest, BeginFrameAck) {
   input.has_damage = has_damage;
 
   BeginFrameAck output;
-  SerializeAndDeserialize<mojom::BeginFrameAck>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::BeginFrameAck>(&input, &output);
 
   EXPECT_EQ(source_id, output.source_id);
   EXPECT_EQ(sequence_number, output.sequence_number);
@@ -185,7 +168,7 @@ TEST_F(StructTraitsTest, FilterOperationBlur) {
   cc::FilterOperation input = cc::FilterOperation::CreateBlurFilter(20);
 
   cc::FilterOperation output;
-  SerializeAndDeserialize<mojom::FilterOperation>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::FilterOperation>(&input, &output);
   ExpectEqual(input, output);
 }
 
@@ -194,7 +177,7 @@ TEST_F(StructTraitsTest, FilterOperationDropShadow) {
       gfx::Point(4, 4), 4.0f, SkColorSetARGB(255, 40, 0, 0));
 
   cc::FilterOperation output;
-  SerializeAndDeserialize<mojom::FilterOperation>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::FilterOperation>(&input, &output);
   ExpectEqual(input, output);
 }
 
@@ -207,7 +190,7 @@ TEST_F(StructTraitsTest, FilterOperationReferenceFilter) {
           nullptr));
 
   cc::FilterOperation output;
-  SerializeAndDeserialize<mojom::FilterOperation>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::FilterOperation>(&input, &output);
   ExpectEqual(input, output);
 }
 
@@ -218,7 +201,7 @@ TEST_F(StructTraitsTest, FilterOperations) {
   input.Append(cc::FilterOperation::CreateZoomFilter(2.0f, 1));
 
   cc::FilterOperations output;
-  SerializeAndDeserialize<mojom::FilterOperations>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::FilterOperations>(&input, &output);
 
   EXPECT_EQ(input.size(), output.size());
   for (size_t i = 0; i < input.size(); ++i) {
@@ -231,7 +214,7 @@ TEST_F(StructTraitsTest, LocalSurfaceId) {
       42, base::UnguessableToken::Deserialize(0x12345678, 0x9abcdef0));
 
   LocalSurfaceId output;
-  SerializeAndDeserialize<mojom::LocalSurfaceId>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::LocalSurfaceId>(&input, &output);
 
   EXPECT_EQ(input, output);
 }
@@ -266,7 +249,8 @@ TEST_F(StructTraitsTest, CopyOutputRequest_BitmapRequest) {
   input->set_source(source);
   EXPECT_TRUE(input->is_scaled());
   std::unique_ptr<CopyOutputRequest> output;
-  SerializeAndDeserialize<mojom::CopyOutputRequest>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::CopyOutputRequest>(&input,
+                                                                &output);
 
   EXPECT_EQ(result_format, output->result_format());
   EXPECT_TRUE(output->is_scaled());
@@ -333,7 +317,8 @@ TEST_F(StructTraitsTest, CopyOutputRequest_TextureRequest) {
           run_loop_for_result.QuitClosure(), result_rect)));
   EXPECT_FALSE(input->is_scaled());
   std::unique_ptr<CopyOutputRequest> output;
-  SerializeAndDeserialize<mojom::CopyOutputRequest>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::CopyOutputRequest>(&input,
+                                                                &output);
 
   EXPECT_EQ(output->result_format(), result_format);
   EXPECT_FALSE(output->is_scaled());
@@ -392,7 +377,7 @@ TEST_F(StructTraitsTest, ResourceSettings) {
   input.use_gpu_memory_buffer_resources = kArbitraryBool;
 
   ResourceSettings output;
-  SerializeAndDeserialize<mojom::ResourceSettings>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::ResourceSettings>(&input, &output);
 
   EXPECT_EQ(input.use_gpu_memory_buffer_resources,
             output.use_gpu_memory_buffer_resources);
@@ -411,7 +396,7 @@ TEST_F(StructTraitsTest, Selection) {
   input.start = start;
   input.end = end;
   Selection<gfx::SelectionBound> output;
-  SerializeAndDeserialize<mojom::Selection>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::Selection>(&input, &output);
   EXPECT_EQ(start, output.start);
   EXPECT_EQ(end, output.end);
 }
@@ -433,7 +418,8 @@ TEST_F(StructTraitsTest, SharedQuadState) {
                    clip_rect, is_clipped, are_contents_opaque, opacity,
                    blend_mode, sorting_context_id);
   SharedQuadState output_sqs;
-  SerializeAndDeserialize<mojom::SharedQuadState>(input_sqs, &output_sqs);
+  mojo::test::SerializeAndDeserialize<mojom::SharedQuadState>(&input_sqs,
+                                                              &output_sqs);
   EXPECT_EQ(quad_to_target_transform, output_sqs.quad_to_target_transform);
   EXPECT_EQ(layer_rect, output_sqs.quad_layer_rect);
   EXPECT_EQ(visible_layer_rect, output_sqs.visible_quad_layer_rect);
@@ -515,7 +501,7 @@ TEST_F(StructTraitsTest, CompositorFrame) {
   input.metadata.begin_frame_ack = begin_frame_ack;
 
   CompositorFrame output;
-  SerializeAndDeserialize<mojom::CompositorFrame>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::CompositorFrame>(&input, &output);
 
   EXPECT_EQ(device_scale_factor, output.metadata.device_scale_factor);
   EXPECT_EQ(root_scroll_offset, output.metadata.root_scroll_offset);
@@ -573,9 +559,9 @@ TEST_F(StructTraitsTest, SurfaceInfo) {
   constexpr float device_scale_factor = 1.234f;
   constexpr gfx::Size size(987, 123);
 
-  const SurfaceInfo input(surface_id, device_scale_factor, size);
+  SurfaceInfo input(surface_id, device_scale_factor, size);
   SurfaceInfo output;
-  SerializeAndDeserialize<mojom::SurfaceInfo>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::SurfaceInfo>(&input, &output);
 
   EXPECT_EQ(input.id(), output.id());
   EXPECT_EQ(input.size_in_pixels(), output.size_in_pixels());
@@ -601,7 +587,7 @@ TEST_F(StructTraitsTest, ReturnedResource) {
   input.lost = lost;
 
   ReturnedResource output;
-  SerializeAndDeserialize<mojom::ReturnedResource>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::ReturnedResource>(&input, &output);
 
   EXPECT_EQ(id, output.id);
   EXPECT_EQ(sync_token, output.sync_token);
@@ -683,7 +669,8 @@ TEST_F(StructTraitsTest, CompositorFrameMetadata) {
 #endif  // defined(OS_ANDROID)
 
   CompositorFrameMetadata output;
-  SerializeAndDeserialize<mojom::CompositorFrameMetadata>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::CompositorFrameMetadata>(&input,
+                                                                      &output);
   EXPECT_EQ(device_scale_factor, output.device_scale_factor);
   EXPECT_EQ(root_scroll_offset, output.root_scroll_offset);
   EXPECT_EQ(page_scale_factor, output.page_scale_factor);
@@ -792,7 +779,7 @@ TEST_F(StructTraitsTest, RenderPass) {
       SK_ColorYELLOW, false);
 
   std::unique_ptr<RenderPass> output;
-  SerializeAndDeserialize<mojom::RenderPass>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::RenderPass>(&input, &output);
 
   EXPECT_EQ(input->quad_list.size(), output->quad_list.size());
   EXPECT_EQ(input->shared_quad_state_list.size(),
@@ -887,7 +874,7 @@ TEST_F(StructTraitsTest, RenderPassWithEmptySharedQuadStateList) {
   // Unlike the previous test, don't add any quads to the list; we need to
   // verify that the serialization code can deal with that.
   std::unique_ptr<RenderPass> output;
-  SerializeAndDeserialize<mojom::RenderPass>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::RenderPass>(&input, &output);
 
   EXPECT_EQ(input->quad_list.size(), output->quad_list.size());
   EXPECT_EQ(input->shared_quad_state_list.size(),
@@ -981,7 +968,7 @@ TEST_F(StructTraitsTest, QuadListBasic) {
                                  resource_id6, resource_size_in_pixels, matrix);
 
   std::unique_ptr<RenderPass> output;
-  SerializeAndDeserialize<mojom::RenderPass>(render_pass->DeepCopy(), &output);
+  mojo::test::SerializeAndDeserialize<mojom::RenderPass>(&render_pass, &output);
 
   ASSERT_EQ(render_pass->quad_list.size(), output->quad_list.size());
 
@@ -1062,7 +1049,7 @@ TEST_F(StructTraitsTest, SurfaceId) {
                                          base::UnguessableToken::Create());
   SurfaceId input(frame_sink_id, local_surface_id);
   SurfaceId output;
-  SerializeAndDeserialize<mojom::SurfaceId>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::SurfaceId>(&input, &output);
   EXPECT_EQ(frame_sink_id, output.frame_sink_id());
   EXPECT_EQ(local_surface_id, output.local_surface_id());
 }
@@ -1100,7 +1087,8 @@ TEST_F(StructTraitsTest, TransferableResource) {
   input.is_overlay_candidate = is_overlay_candidate;
 
   TransferableResource output;
-  SerializeAndDeserialize<mojom::TransferableResource>(input, &output);
+  mojo::test::SerializeAndDeserialize<mojom::TransferableResource>(&input,
+                                                                   &output);
 
   EXPECT_EQ(id, output.id);
   EXPECT_EQ(format, output.format);
@@ -1148,7 +1136,7 @@ TEST_F(StructTraitsTest, YUVDrawQuad) {
                bits_per_channel, require_overlay, is_protected_video);
 
   std::unique_ptr<RenderPass> output;
-  SerializeAndDeserialize<mojom::RenderPass>(render_pass->DeepCopy(), &output);
+  mojo::test::SerializeAndDeserialize<mojom::RenderPass>(&render_pass, &output);
 
   ASSERT_EQ(render_pass->quad_list.size(), output->quad_list.size());
 
@@ -1177,7 +1165,7 @@ TEST_F(StructTraitsTest, CopyOutputResult_Empty) {
   auto input = std::make_unique<CopyOutputResult>(
       CopyOutputResult::Format::RGBA_BITMAP, gfx::Rect());
   std::unique_ptr<CopyOutputResult> output;
-  SerializeAndDeserialize<mojom::CopyOutputResult>(std::move(input), &output);
+  mojo::test::SerializeAndDeserialize<mojom::CopyOutputResult>(&input, &output);
 
   EXPECT_TRUE(output->IsEmpty());
   EXPECT_EQ(output->format(), CopyOutputResult::Format::RGBA_BITMAP);
@@ -1197,7 +1185,7 @@ TEST_F(StructTraitsTest, CopyOutputResult_Bitmap) {
       std::make_unique<CopyOutputSkBitmapResult>(result_rect, bitmap);
 
   std::unique_ptr<CopyOutputResult> output;
-  SerializeAndDeserialize<mojom::CopyOutputResult>(std::move(input), &output);
+  mojo::test::SerializeAndDeserialize<mojom::CopyOutputResult>(&input, &output);
 
   EXPECT_FALSE(output->IsEmpty());
   EXPECT_EQ(output->format(), CopyOutputResult::Format::RGBA_BITMAP);
@@ -1250,7 +1238,7 @@ TEST_F(StructTraitsTest, CopyOutputResult_Texture) {
                                                 std::move(callback));
 
   std::unique_ptr<CopyOutputResult> output;
-  SerializeAndDeserialize<mojom::CopyOutputResult>(std::move(input), &output);
+  mojo::test::SerializeAndDeserialize<mojom::CopyOutputResult>(&input, &output);
 
   EXPECT_FALSE(output->IsEmpty());
   EXPECT_EQ(output->format(), CopyOutputResult::Format::RGBA_TEXTURE);
diff --git a/ui/gfx/BUILD.gn b/ui/gfx/BUILD.gn
index 26c1273598113..4f727ce95fb1e 100644
--- a/ui/gfx/BUILD.gn
+++ b/ui/gfx/BUILD.gn
@@ -774,6 +774,7 @@ test("gfx_unittests") {
       "//cc/paint",
       "//mojo/core/embedder",
       "//mojo/public/cpp/bindings",
+      "//mojo/public/cpp/test_support:test_utils",
       "//ui/gfx/geometry/mojo:unit_test",
       "//ui/gfx/image/mojo:unit_test",
       "//ui/gfx/mojo:test_interfaces",
diff --git a/ui/gfx/mojo/struct_traits_unittest.cc b/ui/gfx/mojo/struct_traits_unittest.cc
index 9f6841400885b..56cef9be17e49 100644
--- a/ui/gfx/mojo/struct_traits_unittest.cc
+++ b/ui/gfx/mojo/struct_traits_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/message_loop/message_loop.h"
 #include "build/build_config.h"
 #include "mojo/public/cpp/bindings/binding_set.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/gfx/mojo/accelerated_widget_struct_traits.h"
 #include "ui/gfx/mojo/buffer_types_struct_traits.h"
@@ -29,15 +30,6 @@ gfx::AcceleratedWidget CastToAcceleratedWidget(int i) {
 #endif
 }
 
-// Test StructTrait serialization and deserialization for copyable type. |input|
-// will be serialized and then deserialized into |output|.
-template <class MojomType, class Type>
-void SerializeAndDeserialize(const Type& input, Type* output) {
-  MojomType::DeserializeFromMessage(
-      mojo::Message(MojomType::SerializeAsMessage(&input).TakeMojoMessage()),
-      output);
-}
-
 class StructTraitsTest : public testing::Test, public mojom::TraitsTestService {
  public:
   StructTraitsTest() {}
@@ -148,7 +140,8 @@ TEST_F(StructTraitsTest, Transform) {
 TEST_F(StructTraitsTest, MAYBE_AcceleratedWidget) {
   gfx::AcceleratedWidget input(CastToAcceleratedWidget(1001));
   gfx::AcceleratedWidget output;
-  SerializeAndDeserialize<gfx::mojom::AcceleratedWidget>(input, &output);
+  mojo::test::SerializeAndDeserialize<gfx::mojom::AcceleratedWidget>(&input,
+                                                                     &output);
   EXPECT_EQ(input, output);
 }
 
@@ -236,7 +229,8 @@ TEST_F(StructTraitsTest, PresentationFeedback) {
       PresentationFeedback::kVSync | PresentationFeedback::kZeroCopy;
   PresentationFeedback input{timestamp, interval, flags};
   PresentationFeedback output;
-  SerializeAndDeserialize<gfx::mojom::PresentationFeedback>(input, &output);
+  mojo::test::SerializeAndDeserialize<gfx::mojom::PresentationFeedback>(
+      &input, &output);
   EXPECT_EQ(timestamp, output.timestamp);
   EXPECT_EQ(interval, output.interval);
   EXPECT_EQ(flags, output.flags);