Support converting non-canonical extensions in upb message conversion.

PiperOrigin-RevId: 966915077
diff --git a/upb/message/BUILD b/upb/message/BUILD
index d2d02ac..174431b 100644
--- a/upb/message/BUILD
+++ b/upb/message/BUILD
@@ -279,6 +279,7 @@
         ":compare",
         ":internal",
         ":message",
+        ":message_unknowns",
         "//upb/base",
         "//upb/mem",
         "//upb/mini_table",
@@ -302,6 +303,7 @@
         ":message",
         ":message_test_upb_minitable_proto",
         ":message_test_upb_proto",
+        ":message_unknowns",
         ":promote",
         "//upb/base",
         "//upb/mem",
diff --git a/upb/message/convert.c b/upb/message/convert.c
index 4520928..e8585f6 100644
--- a/upb/message/convert.c
+++ b/upb/message/convert.c
@@ -18,9 +18,11 @@
 #include "upb/message/array.h"
 #include "upb/message/compare.h"
 #include "upb/message/internal/accessors.h"
+#include "upb/message/internal/extension.h"
 #include "upb/message/internal/message.h"
 #include "upb/message/map.h"
 #include "upb/message/message.h"
+#include "upb/message/unknown_fields.h"
 #include "upb/mini_table/enum.h"
 #include "upb/mini_table/extension.h"
 #include "upb/mini_table/extension_registry.h"
@@ -110,24 +112,6 @@
   }
 }
 
-static void upb_Message_EncodeExtensionAsUnknown(
-    upb_encstate* e, upb_Message* dst, const upb_MiniTable* dst_mt,
-    const upb_MiniTableExtension* ext, upb_MessageValue val, int depth,
-    int options, upb_ErrorHandler* err) {
-  size_t size;
-  int encode_options = upb_Encode_LimitDepth(options, depth);
-  bool is_message_set = upb_MiniTable_IsMessageSet(dst_mt);
-  char* buf = upb_BackAlloc_Init(&e->alloc, e->alloc.arena);
-  UPB_PRIVATE(_upb_Encode_Extension)(e, ext, val, is_message_set, &buf, &size,
-                                     encode_options);
-  if (size > 0) {
-    if (!UPB_PRIVATE(_upb_Message_AddUnknown)(dst, buf, size, e->alloc.arena,
-                                              kUpb_AddUnknown_Alias)) {
-      upb_ErrorHandler_ThrowError(err, kUpb_ErrorCode_OutOfMemory);
-    }
-  }
-}
-
 static void upb_Message_ConvertInternal(upb_Converter* c, upb_Message* dst,
                                         const upb_Message* src,
                                         const upb_MiniTable* dst_mt,
@@ -395,6 +379,139 @@
   }
 }
 
+static void upb_Message_ConvertExtension(upb_Converter* c, upb_Message* dst,
+                                         const upb_MiniTable* dst_mt,
+                                         const upb_MiniTableExtension* ext,
+                                         upb_MessageValue val,
+                                         const upb_ExtensionRegistry* extreg,
+                                         int depth) {
+  const upb_MiniTableField* dst_f = upb_MiniTable_FindFieldByNumber(
+      dst_mt, upb_MiniTableExtension_Number(ext));
+  const upb_MiniTableExtension* dst_ext = NULL;
+  if (!dst_f) {
+    // Source extension not found in the destination schema. Check the
+    // extension registry.
+    if (extreg != NULL) {
+      dst_ext = upb_ExtensionRegistry_Lookup(
+          extreg, dst_mt, upb_MiniTableExtension_Number(ext));
+      if (dst_ext) {
+        dst_f = upb_MiniTableExtension_ToField(dst_ext);
+      }
+    }
+  }
+
+  if (dst_f == NULL) {
+    // Since this extension is not known in the destination schema, convert it
+    // to a non-canonical extension.
+    upb_Extension* msg_ext = UPB_PRIVATE(
+        _upb_Message_CreateNonCanonicalExtension)(dst, ext, c->arena);
+    if (!msg_ext) {
+      upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+    }
+    msg_ext->data = val;
+    return;
+  }
+
+  const upb_MiniTableField* src_f = upb_MiniTableExtension_ToField(ext);
+
+  UPB_ASSERT(!upb_MiniTableField_IsMap(src_f));
+  if (UPB_UNLIKELY(!_upb_MiniTableField_IsExtensionCompatible(src_f, dst_f))) {
+    // Return an error due to type mismatch.
+    upb_ErrorHandler_ThrowError(&c->err, kUpb_ConvertStatus_Incompatible);
+  }
+
+  if (upb_MiniTableField_CType(dst_f) == kUpb_CType_Message) {
+    // If the destination is an extension in the registry (dst_ext), we must use
+    // upb_MiniTableExtension_GetSubMessage() since extensions store submessage
+    // layouts directly. Otherwise, it is a normal field declared in the target
+    // MiniTable, and we use upb_MiniTable_SubMessage(). Similarly, the source
+    // ext behaves as an extension.
+    const upb_MiniTable* dst_sub_mt =
+        dst_ext ? upb_MiniTableExtension_GetSubMessage(dst_ext)
+                : upb_MiniTable_SubMessage(dst_f);
+    const upb_MiniTable* src_sub_mt = upb_MiniTableExtension_GetSubMessage(ext);
+
+    if (upb_MiniTableField_IsArray(dst_f)) {
+      if (dst_sub_mt != src_sub_mt) {
+        // Array of messages, and the sub message types differ. Perform
+        // conversion.
+        upb_Array* dst_arr = upb_Array_New(c->arena, kUpb_CType_Message);
+        if (!dst_arr)
+          upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+        upb_Array_DeepConvert(c, dst_arr, val.array_val, dst_sub_mt, src_sub_mt,
+                              dst_f, dst, extreg, depth);
+        upb_MessageValue valid_val;
+        valid_val.array_val = dst_arr;
+        if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &valid_val,
+                                             c->arena)) {
+          upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+        }
+      } else {
+        // Array of messages, and the sub message types are the same.
+        // Shallow copy.
+        if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &val,
+                                             c->arena)) {
+          upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+        }
+      }
+    } else if (dst_sub_mt == src_sub_mt) {
+      // Scalar message, and the message types are the same.
+      // Shallow copy.
+      if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &val,
+                                           c->arena)) {
+        upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+      }
+    } else {
+      // Scalar message, and the message types differ. Perform conversion.
+      upb_Message* dst_sub = upb_Message_New(dst_sub_mt, c->arena);
+      if (!dst_sub)
+        upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+
+      upb_Message_ConvertInternal(c, dst_sub, val.msg_val, dst_sub_mt,
+                                  src_sub_mt, extreg, depth);
+
+      upb_MessageValue valid_val;
+      valid_val.msg_val = dst_sub;
+      if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &valid_val,
+                                           c->arena)) {
+        upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+      }
+    }
+  } else {
+    // Scalar non-message type.
+    if (upb_MiniTableField_IsClosedEnum(dst_f)) {
+      if (upb_MiniTableField_IsArray(dst_f)) {
+        upb_Array* dst_arr = upb_Array_New(c->arena, kUpb_CType_Int32);
+        if (!dst_arr)
+          upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+        upb_Array_DeepConvert(c, dst_arr, val.array_val, NULL, NULL, dst_f, dst,
+                              extreg, depth);
+        upb_MessageValue valid_val;
+        valid_val.array_val = dst_arr;
+        if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &valid_val,
+                                             c->arena)) {
+          upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+        }
+        return;
+      } else {
+        const upb_MiniTableEnum* dst_e =
+            dst_ext ? upb_MiniTableExtension_GetSubEnum(dst_ext)
+                    : upb_MiniTable_GetSubEnumTable(dst_f);
+        if (!upb_MiniTableEnum_CheckValue(dst_e, val.int32_val)) {
+          if (!_upb_Encoder_AddEnumValueToUnknown(dst, dst_f, val.int32_val,
+                                                  c->arena)) {
+            upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+          }
+          return;
+        }
+      }
+    }
+    if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &val, c->arena)) {
+      upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
+    }
+  }
+}
+
 static void upb_Message_ConvertExtensions(upb_Converter* c, upb_Message* dst,
                                           const upb_Message* src,
                                           const upb_MiniTable* dst_mt,
@@ -404,123 +521,7 @@
   upb_MessageValue val;
   uintptr_t iter = kUpb_Message_ExtensionBegin;
   while (upb_Message_NextExtension(src, &ext, &val, &iter)) {
-    const upb_MiniTableField* dst_f = upb_MiniTable_FindFieldByNumber(
-        dst_mt, upb_MiniTableExtension_Number(ext));
-    const upb_MiniTableExtension* dst_ext = NULL;
-    if (!dst_f) {
-      // Source extension not found in the destination schema. Check the
-      // extension registry.
-      if (extreg != NULL) {
-        dst_ext = upb_ExtensionRegistry_Lookup(
-            extreg, dst_mt, upb_MiniTableExtension_Number(ext));
-        if (dst_ext) {
-          dst_f = upb_MiniTableExtension_ToField(dst_ext);
-        }
-      }
-    }
-
-    if (dst_f) {
-      const upb_MiniTableField* src_f = upb_MiniTableExtension_ToField(ext);
-
-      UPB_ASSERT(!upb_MiniTableField_IsMap(src_f));
-      if (UPB_UNLIKELY(
-              !_upb_MiniTableField_IsExtensionCompatible(src_f, dst_f))) {
-        // Return an error due to type mismatch.
-        upb_ErrorHandler_ThrowError(&c->err, kUpb_ConvertStatus_Incompatible);
-      }
-
-      if (upb_MiniTableField_CType(dst_f) == kUpb_CType_Message) {
-        const upb_MiniTable* dst_sub_mt = upb_MiniTable_SubMessage(dst_f);
-        const upb_MiniTable* src_sub_mt = upb_MiniTable_SubMessage(src_f);
-
-        if (upb_MiniTableField_IsArray(dst_f)) {
-          if (dst_sub_mt != src_sub_mt) {
-            // Array of messages, and the sub message types differ. Perform
-            // conversion.
-            upb_Array* dst_arr = upb_Array_New(c->arena, kUpb_CType_Message);
-            if (!dst_arr)
-              upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-            upb_Array_DeepConvert(c, dst_arr, val.array_val, dst_sub_mt,
-                                  src_sub_mt, dst_f, dst, extreg, depth);
-            upb_MessageValue valid_val;
-            valid_val.array_val = dst_arr;
-            if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext,
-                                                 &valid_val, c->arena)) {
-              upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-            }
-          } else {
-            // Array of messages, and the sub message types are the same.
-            // Shallow copy.
-            if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &val,
-                                                 c->arena)) {
-              upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-            }
-          }
-        } else if (dst_sub_mt == src_sub_mt) {
-          // Scalar message, and the message types are the same.
-          // Shallow copy.
-          if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &val,
-                                               c->arena)) {
-            upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-          }
-        } else {
-          // Scalar message, and the message types differ. Perform conversion.
-          upb_Message* dst_sub = upb_Message_New(dst_sub_mt, c->arena);
-          if (!dst_sub)
-            upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-
-          upb_Message_ConvertInternal(c, dst_sub, val.msg_val, dst_sub_mt,
-                                      src_sub_mt, extreg, depth);
-
-          upb_MessageValue valid_val;
-          valid_val.msg_val = dst_sub;
-          if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &valid_val,
-                                               c->arena)) {
-            upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-          }
-        }
-      } else {
-        // Scalar non-message type.
-        if (upb_MiniTableField_IsClosedEnum(dst_f)) {
-          if (upb_MiniTableField_IsArray(dst_f)) {
-            upb_Array* dst_arr = upb_Array_New(c->arena, kUpb_CType_Int32);
-            if (!dst_arr)
-              upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-            upb_Array_DeepConvert(c, dst_arr, val.array_val, NULL, NULL, dst_f,
-                                  dst, extreg, depth);
-            upb_MessageValue valid_val;
-            valid_val.array_val = dst_arr;
-            if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext,
-                                                 &valid_val, c->arena)) {
-              upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-            }
-            continue;
-          } else {
-            const upb_MiniTableEnum* dst_e =
-                dst_ext ? upb_MiniTableExtension_GetSubEnum(dst_ext)
-                        : upb_MiniTable_GetSubEnumTable(dst_f);
-            if (!upb_MiniTableEnum_CheckValue(dst_e, val.int32_val)) {
-              if (!_upb_Encoder_AddEnumValueToUnknown(dst, dst_f, val.int32_val,
-                                                      c->arena)) {
-                upb_ErrorHandler_ThrowError(&c->err,
-                                            kUpb_ErrorCode_OutOfMemory);
-              }
-              continue;
-            }
-          }
-        }
-        if (!upb_Message_SetFieldOrExtension(dst, dst_f, dst_ext, &val,
-                                             c->arena)) {
-          upb_ErrorHandler_ThrowError(&c->err, kUpb_ErrorCode_OutOfMemory);
-        }
-      }
-    } else {
-      // Since this extension is not known in the destination schema, encode it
-      // as an unknown field.
-      // TODO - b/510055656: to handle this as a non-canonical extension
-      upb_Message_EncodeExtensionAsUnknown(&c->encoder, dst, dst_mt, ext, val,
-                                           depth, c->encode_options, &c->err);
-    }
+    upb_Message_ConvertExtension(c, dst, dst_mt, ext, val, extreg, depth);
   }
 }
 
@@ -596,20 +597,28 @@
   }
 
   // Convert unknown fields.
-  upb_StringView data;
-  size_t iter = kUpb_Message_UnknownBegin;
-  while (upb_Message_NextUnknown(src, &data, &iter)) {
-    int decode_options = upb_Decode_LimitDepth(
-        c->decode_options | kUpb_DecodeOption_AliasString, depth);
+  upb_MessageUnknown unknown;
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  while (upb_Message_NextUnknown2(src, &unknown, &iter)) {
+    if (unknown.type == kUpb_MessageUnknownType_StringView) {
+      upb_StringView data = unknown.value.bytes;
+      int decode_options = upb_Decode_LimitDepth(
+          c->decode_options | kUpb_DecodeOption_AliasString, depth);
 
-    // Reuse d. Reset input stream.
-    const char* ptr = data.data;
-    upb_Decoder* d = &c->decoder;
-    upb_EpsCopyInputStream_InitWithErrorHandler(&d->input, &ptr, data.size,
-                                                d->err);
-    upb_Decoder_Reset(d, decode_options, dst);
-    _upb_Decoder_DecodeMessage(d, ptr, dst, dst_mt);
-    UPB_ASSERT(d->end_group == DECODE_NOGROUP);
+      // Reuse d. Reset input stream.
+      const char* ptr = data.data;
+      upb_Decoder* d = &c->decoder;
+      upb_EpsCopyInputStream_InitWithErrorHandler(&d->input, &ptr, data.size,
+                                                  d->err);
+      upb_Decoder_Reset(d, decode_options, dst);
+      _upb_Decoder_DecodeMessage(d, ptr, dst, dst_mt);
+      UPB_ASSERT(d->end_group == DECODE_NOGROUP);
+    } else {
+      UPB_ASSERT(unknown.type == kUpb_MessageUnknownType_NonCanonicalExtension);
+      const upb_Extension* ext = unknown.value.extension;
+      upb_Message_ConvertExtension(c, dst, dst_mt, ext->ext, ext->data, extreg,
+                                   depth);
+    }
   }
 }
 
diff --git a/upb/message/convert_test.cc b/upb/message/convert_test.cc
index f75bdcf..f4ed1e6 100644
--- a/upb/message/convert_test.cc
+++ b/upb/message/convert_test.cc
@@ -27,12 +27,15 @@
 #include "upb/message/map.h"
 #include "upb/message/message.h"
 #include "upb/message/test.upb_minitable.h"
+#include "upb/message/unknown_fields.h"
+#include "upb/mini_table/extension.h"
 #include "upb/mini_table/extension_registry.h"
 #include "upb/mini_table/message.h"
 #include "upb/wire/decode.h"
 
 // Must be last to ensure UPB_PRIVATE is defined.
 #include "upb/port/def.inc"
+#include "upb/wire/encode.h"
 
 // We use the generated upb_MiniTables from test_messages_proto3.
 #define TEST_MT &protobuf_0test_0messages__proto3__TestAllTypesProto3_msg_init
@@ -148,13 +151,15 @@
 
   // Dst should have unknown field 1 with value 999.
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst, &data, &iter));
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   EXPECT_GE(data.size, 3);
   EXPECT_EQ((uint8_t)data.data[0], 8);
   EXPECT_EQ((uint8_t)data.data[1], 0xE7);
   EXPECT_EQ((uint8_t)data.data[2], 0x07);
-  EXPECT_FALSE(upb_Message_NextUnknown(dst, &data, &iter));
+  EXPECT_FALSE(upb_Message_NextUnknown2(dst, &unknown, &iter));
 }
 
 TEST(ConvertTest, DeepConvertMap) {
@@ -732,15 +737,16 @@
       UPB_UPCAST(msg), src_mt, dst_mt, nullptr, 0, 0, arena.ptr());
   ASSERT_NE(dst_msg, nullptr);
 
-  // Dst should have unknown field 1000 with value 123.
-  size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
-  EXPECT_EQ(data.size, 3);
-  EXPECT_EQ((uint8_t)data.data[0], 0xC0);
-  EXPECT_EQ((uint8_t)data.data[1], 0x3E);
-  EXPECT_EQ((uint8_t)data.data[2], 0x7B);
-  EXPECT_FALSE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  // Dst should have a non-canonical extension of field 1000 with value 123.
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_NonCanonicalExtension);
+  const upb_Extension* ext_found =
+      static_cast<const upb_Extension*>(unknown.value.extension);
+  EXPECT_EQ(upb_MiniTableExtension_Number(ext_found->ext), 1000);
+  EXPECT_EQ(ext_found->data.int32_val, 123);
+  EXPECT_FALSE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
 }
 
 TEST(ConvertTest, ConvertExtensionToExtendableButUnknown) {
@@ -766,16 +772,180 @@
   // The destination message (AnotherMessageWithExtension) supports extensions,
   // but since we did not provide an extension registry containing field 1000 to
   // the conversion, the extension is treated as unknown/non-canonical in the
-  // destination schemas. Therefore, it should be encoded as an unknown field
-  // (field 1000 with value 123).
-  size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
-  EXPECT_EQ(data.size, 3);
-  EXPECT_EQ((uint8_t)data.data[0], 0xC0);
-  EXPECT_EQ((uint8_t)data.data[1], 0x3E);
-  EXPECT_EQ((uint8_t)data.data[2], 0x7B);
-  EXPECT_FALSE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  // destination schemas. Therefore, it should be handled/stored as a
+  // non-canonical extension.
+  uintptr_t iter2 = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown2;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown2, &iter2));
+  EXPECT_EQ(unknown2.type, kUpb_MessageUnknownType_NonCanonicalExtension);
+  const upb_Extension* ext_found2 =
+      static_cast<const upb_Extension*>(unknown2.value.extension);
+  EXPECT_EQ(upb_MiniTableExtension_Number(ext_found2->ext), 1000);
+  EXPECT_EQ(ext_found2->data.int32_val, 123);
+  EXPECT_FALSE(upb_Message_NextUnknown2(dst_msg, &unknown2, &iter2));
+}
+
+TEST(ConvertTest, NonCanonicalToNonCanonical) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 123;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_field_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__AnotherMessageWithExtension_msg_init;
+
+  // Convert without registry to obtain a non-canonical extension source.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert without registry again. It should remain non-canonical.
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, dst_mt, src_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_NonCanonicalExtension);
+  const upb_Extension* ext_found =
+      static_cast<const upb_Extension*>(unknown.value.extension);
+  EXPECT_EQ(upb_MiniTableExtension_Number(ext_found->ext), 1000);
+  EXPECT_EQ(ext_found->data.int32_val, 123);
+  EXPECT_FALSE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+}
+
+TEST(ConvertTest, NonCanonicalToCanonical) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 123;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_field_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__AnotherMessageWithExtension_msg_init;
+
+  // Convert without registry to obtain a non-canonical extension source.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert with registry. It should become a canonical extension.
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg,
+                            upb_test_convert_another_ext_field_int32_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, dst_mt, dst_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  const upb_test_convert_AnotherMessageWithExtension* dst =
+      (const upb_test_convert_AnotherMessageWithExtension*)converted;
+  EXPECT_TRUE(upb_Message_HasExtension(
+      UPB_UPCAST(dst), upb_test_convert_another_ext_field_int32_ext));
+  int32_t out_val = upb_Message_GetExtensionInt32(
+      UPB_UPCAST(dst), upb_test_convert_another_ext_field_int32_ext, 0);
+  EXPECT_EQ(123, out_val);
+
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown;
+  EXPECT_FALSE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+}
+
+TEST(ConvertTest, NonCanonicalToNormalField) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 123;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_field_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__AnotherMessageWithExtension_msg_init;
+
+  // Convert without registry to obtain a non-canonical extension source.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert to MessageWithKnown. Field 1000 is a normal field.
+  const upb_MiniTable* known_mt =
+      &upb__test__convert__MessageWithKnown_msg_init;
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, dst_mt, known_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  const upb_test_convert_MessageWithKnown* dst =
+      (const upb_test_convert_MessageWithKnown*)converted;
+  EXPECT_TRUE(upb_test_convert_MessageWithKnown_has_known_field_int32(dst));
+  EXPECT_EQ(123, upb_test_convert_MessageWithKnown_known_field_int32(dst));
+
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown;
+  EXPECT_FALSE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+}
+
+TEST(ConvertTest, NonCanonicalToUnknownBytes) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 123;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_field_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__AnotherMessageWithExtension_msg_init;
+
+  // Convert without registry to obtain a non-canonical extension source.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Serialize the non_canonical_src to wire bytes.
+  char* wire_buf;
+  size_t wire_size;
+  upb_EncodeStatus encode_status = upb_Encode(
+      non_canonical_src, dst_mt, 0, arena.ptr(), &wire_buf, &wire_size);
+  ASSERT_EQ(encode_status, kUpb_EncodeStatus_Ok);
+
+  // Decode the bytes back to AnotherMessageWithExtension without registry.
+  // The decoder should decode the extension as unknown bytes (type Bytes).
+  upb_Message* decoded = upb_Message_New(dst_mt, arena.ptr());
+  ASSERT_NE(decoded, nullptr);
+  upb_DecodeStatus decode_status =
+      upb_Decode(wire_buf, wire_size, decoded, dst_mt, nullptr, 0, arena.ptr());
+  ASSERT_EQ(decode_status, kUpb_DecodeStatus_Ok);
+
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(decoded, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  EXPECT_GT(unknown.value.bytes.size, 0);
+  EXPECT_FALSE(upb_Message_NextUnknown2(decoded, &unknown, &iter));
 }
 
 TEST(ConvertTest, OneofDemotion) {
@@ -798,8 +968,10 @@
 
   // Dst should have unknown field 2 with value "test".
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  EXPECT_TRUE(upb_Message_NextUnknown(dst, &data, &iter));
+  upb_MessageUnknown unknown;
+  EXPECT_TRUE(upb_Message_NextUnknown2(dst, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   // "test" string field 2.
   // Tag 2, type 2: 2<<3 | 2 = 18
   EXPECT_EQ(data.size, 6);
@@ -809,7 +981,7 @@
   EXPECT_EQ((uint8_t)data.data[3], 'e');
   EXPECT_EQ((uint8_t)data.data[4], 's');
   EXPECT_EQ((uint8_t)data.data[5], 't');
-  EXPECT_FALSE(upb_Message_NextUnknown(dst, &data, &iter));
+  EXPECT_FALSE(upb_Message_NextUnknown2(dst, &unknown, &iter));
 
   // Now convert back and verify.
   const upb_Message* dst2_msg =
@@ -864,8 +1036,10 @@
       upb_test_convert_Proto2EnumMessage_has_optional_nested_enum(dst));
 
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   EXPECT_GT(data.size, 0);
 
   protobuf_test_messages_proto3_TestAllTypesProto3* check_msg =
@@ -943,8 +1117,10 @@
       dst, upb_StringView_FromString("invalid"), &val));
 
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   EXPECT_GT(data.size, 0);
 
   // Verify that the unknown field contains the complete invalid map entry.
@@ -1020,8 +1196,10 @@
   EXPECT_EQ(upb_test_convert_Proto2EnumMessage_BAR, values[0]);
 
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   EXPECT_GT(data.size, 0);
 
   protobuf_test_messages_proto3_TestAllTypesProto3* check_msg =
@@ -1397,8 +1575,10 @@
   EXPECT_FALSE(upb_test_convert_MessageWithKnownEnum_has_ext_enum(dst));
 
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   EXPECT_GT(data.size, 0);
 
   // We cannot parse it back using the extension registry because the extension
@@ -1486,8 +1666,10 @@
   EXPECT_EQ(upb_test_convert_Proto2EnumMessage_BAR, values[0]);
 
   size_t iter = kUpb_Message_UnknownBegin;
-  upb_StringView data;
-  ASSERT_TRUE(upb_Message_NextUnknown(dst_msg, &data, &iter));
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(dst_msg, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+  upb_StringView data = unknown.value.bytes;
   EXPECT_GT(data.size, 0);
 
   // We cannot parse it back using the extension registry because the extension
@@ -1554,3 +1736,547 @@
   upb_StringView str = upb_test_convert_SrcWithOneof_oneof_string(dst);
   EXPECT_EQ(std::string("abc"), std::string(str.data, str.size));
 }
+
+TEST(ConvertTest, NonCanonicalMessageToCanonical_SameSchema) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_test_convert_MessageWithInt32* sub =
+      upb_test_convert_MessageWithInt32_new(arena.ptr());
+  upb_test_convert_MessageWithInt32_set_f1(sub, 123);
+
+  upb_MessageValue ext_val;
+  ext_val.msg_val = UPB_UPCAST(sub);
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_field_msg_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  // Convert to carrier to make it non-canonical.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert back to MessageWithExtension with registry.
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_field_msg_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  // Verify canonical extension.
+  EXPECT_TRUE(
+      upb_Message_HasExtension(converted, upb_test_convert_ext_field_msg_ext));
+
+  upb_Message* dst_sub = upb_Message_GetExtensionMessage(
+      converted, upb_test_convert_ext_field_msg_ext, nullptr);
+  ASSERT_NE(dst_sub, nullptr);
+
+  const upb_test_convert_MessageWithInt32* typed_dst_sub =
+      (const upb_test_convert_MessageWithInt32*)dst_sub;
+  EXPECT_EQ(123, upb_test_convert_MessageWithInt32_f1(typed_dst_sub));
+
+  // Should be shallow linked (pointer identity) since schemas match.
+  EXPECT_EQ((const void*)dst_sub, (const void*)sub);
+}
+
+TEST(ConvertTest, NonCanonicalMessageToCanonical_DiffSchema) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_test_convert_MessageWithInt32* sub =
+      upb_test_convert_MessageWithInt32_new(arena.ptr());
+  upb_test_convert_MessageWithInt32_set_f1(sub, 123);
+
+  upb_MessageValue ext_val;
+  ext_val.msg_val = UPB_UPCAST(sub);
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_field_msg_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  // Convert to carrier to make it non-canonical.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert to AnotherMessageWithExtension with registry containing
+  // another_ext_field_msg.
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__AnotherMessageWithExtension_msg_init;
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_another_ext_field_msg_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, dst_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  // Verify canonical extension.
+  EXPECT_TRUE(upb_Message_HasExtension(
+      converted, upb_test_convert_another_ext_field_msg_ext));
+
+  upb_Message* dst_sub = upb_Message_GetExtensionMessage(
+      converted, upb_test_convert_another_ext_field_msg_ext, nullptr);
+  ASSERT_NE(dst_sub, nullptr);
+
+  // Should be MessageWithInt32Clone.
+  const upb_test_convert_MessageWithInt32Clone* typed_dst_sub =
+      (const upb_test_convert_MessageWithInt32Clone*)dst_sub;
+  EXPECT_EQ(123, upb_test_convert_MessageWithInt32Clone_f1(typed_dst_sub));
+
+  // Should be deep copied (different pointer) since schemas differ.
+  EXPECT_NE((const void*)dst_sub, (const void*)sub);
+}
+
+TEST(ConvertTest, NonCanonicalMessageToNormalField_DiffSchema) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_test_convert_MessageWithInt32* sub =
+      upb_test_convert_MessageWithInt32_new(arena.ptr());
+  upb_test_convert_MessageWithInt32_set_f1(sub, 123);
+
+  upb_MessageValue ext_val;
+  ext_val.msg_val = UPB_UPCAST(sub);
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_field_msg_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  // Convert to carrier to make it non-canonical.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert to MessageWithKnownMsgClone.
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__MessageWithKnownMsgClone_msg_init;
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  // Verify normal field.
+  const upb_test_convert_MessageWithKnownMsgClone* dst =
+      (const upb_test_convert_MessageWithKnownMsgClone*)converted;
+
+  const upb_test_convert_MessageWithInt32Clone* dst_sub =
+      upb_test_convert_MessageWithKnownMsgClone_known_msg(dst);
+  ASSERT_NE(dst_sub, nullptr);
+  EXPECT_EQ(123, upb_test_convert_MessageWithInt32Clone_f1(dst_sub));
+
+  // Should be deep copied.
+  EXPECT_NE((const void*)dst_sub, (const void*)sub);
+}
+
+TEST(ConvertTest, NonCanonicalMessageToNonCanonical) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_test_convert_MessageWithInt32* sub =
+      upb_test_convert_MessageWithInt32_new(arena.ptr());
+  upb_test_convert_MessageWithInt32_set_f1(sub, 123);
+
+  upb_MessageValue ext_val;
+  ext_val.msg_val = UPB_UPCAST(sub);
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_field_msg_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  // Convert to carrier to make it non-canonical.
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  // Convert back to src_mt without registry. It should remain non-canonical.
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  upb_MessageUnknown unknown;
+  ASSERT_TRUE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_NonCanonicalExtension);
+  const upb_Extension* ext_found =
+      static_cast<const upb_Extension*>(unknown.value.extension);
+  EXPECT_EQ(upb_MiniTableExtension_Number(ext_found->ext), 1002);
+
+  const upb_test_convert_MessageWithInt32* converted_sub =
+      (const upb_test_convert_MessageWithInt32*)ext_found->data.msg_val;
+  EXPECT_EQ(123, upb_test_convert_MessageWithInt32_f1(converted_sub));
+
+  EXPECT_FALSE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+}
+
+TEST(ConvertTest, NonCanonicalRepeatedInt32ToCanonical) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_Array* arr = upb_Array_New(arena.ptr(), kUpb_CType_Int32);
+  upb_MessageValue val1, val2;
+  val1.int32_val = 123;
+  val2.int32_val = 456;
+  upb_Array_Append(arr, val1, arena.ptr());
+  upb_Array_Append(arr, val2, arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.array_val = arr;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_repeated_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_repeated_int32_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  const upb_Array* dst_arr = upb_Message_GetExtensionArray(
+      converted, upb_test_convert_ext_repeated_int32_ext);
+  ASSERT_NE(dst_arr, nullptr);
+  ASSERT_EQ(2, upb_Array_Size(dst_arr));
+  EXPECT_EQ(123, upb_Array_Get(dst_arr, 0).int32_val);
+  EXPECT_EQ(456, upb_Array_Get(dst_arr, 1).int32_val);
+}
+
+TEST(ConvertTest, NonCanonicalRepeatedInt32ToNormalField) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_Array* arr = upb_Array_New(arena.ptr(), kUpb_CType_Int32);
+  upb_MessageValue val1, val2;
+  val1.int32_val = 123;
+  val2.int32_val = 456;
+  upb_Array_Append(arr, val1, arena.ptr());
+  upb_Array_Append(arr, val2, arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.array_val = arr;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_repeated_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__MessageWithKnownRepeatedInt32_msg_init;
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  const upb_test_convert_MessageWithKnownRepeatedInt32* dst =
+      (const upb_test_convert_MessageWithKnownRepeatedInt32*)converted;
+  size_t len;
+  const int32_t* dst_vals =
+      upb_test_convert_MessageWithKnownRepeatedInt32_known_repeated_int32(dst,
+                                                                          &len);
+  ASSERT_EQ(2, len);
+  EXPECT_EQ(123, dst_vals[0]);
+  EXPECT_EQ(456, dst_vals[1]);
+}
+
+TEST(ConvertTest, NonCanonicalStringToCanonical) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_StringView str = upb_StringView_FromString("hello");
+  upb_MessageValue ext_val;
+  ext_val.str_val = str;
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_string_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_string_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  EXPECT_TRUE(
+      upb_Message_HasExtension(converted, upb_test_convert_ext_string_ext));
+
+  upb_StringView dst_str =
+      upb_Message_GetExtensionString(converted, upb_test_convert_ext_string_ext,
+                                     upb_StringView_FromString(""));
+  EXPECT_EQ(std::string("hello"), std::string(dst_str.data, dst_str.size));
+}
+
+TEST(ConvertTest, NonCanonicalStringToNormalField) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_StringView str = upb_StringView_FromString("hello");
+  upb_MessageValue ext_val;
+  ext_val.str_val = str;
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_string_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__MessageWithKnownString_msg_init;
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  const upb_test_convert_MessageWithKnownString* dst =
+      (const upb_test_convert_MessageWithKnownString*)converted;
+
+  upb_StringView dst_str =
+      upb_test_convert_MessageWithKnownString_known_string(dst);
+  EXPECT_EQ(std::string("hello"), std::string(dst_str.data, dst_str.size));
+}
+
+TEST(ConvertTest, NonCanonicalEnumToCanonical_Valid) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 2;  // BAZ
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_enum_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_enum_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  EXPECT_TRUE(
+      upb_Message_HasExtension(converted, upb_test_convert_ext_enum_ext));
+
+  int32_t dst_val = upb_Message_GetExtensionInt32(
+      converted, upb_test_convert_ext_enum_ext, 0);
+  EXPECT_EQ(2, dst_val);
+}
+
+TEST(ConvertTest, NonCanonicalEnumToCanonical_InvalidClosed) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 99;  // Invalid for Proto2EnumMessage.NestedEnum
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_enum_ext,
+                           &ext_val, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_enum_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  // Should NOT be set as canonical extension.
+  EXPECT_FALSE(
+      upb_Message_HasExtension(converted, upb_test_convert_ext_enum_ext));
+
+  // Should be in unknowns.
+  upb_MessageUnknown unknown;
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  ASSERT_TRUE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+}
+
+TEST(ConvertTest, NonCanonicalRepeatedEnumToCanonical_Mixed) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_Array* arr = upb_Array_New(arena.ptr(), kUpb_CType_Enum);
+  upb_MessageValue val1, val2, val3;
+  val1.int32_val = 1;   // BAR
+  val2.int32_val = 99;  // Invalid
+  val3.int32_val = 2;   // BAZ
+  upb_Array_Append(arr, val1, arena.ptr());
+  upb_Array_Append(arr, val2, arena.ptr());
+  upb_Array_Append(arr, val3, arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.array_val = arr;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_repeated_enum_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_repeated_enum_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  // Canonical extension should have [1, 2].
+  const upb_Array* dst_arr = upb_Message_GetExtensionArray(
+      converted, upb_test_convert_ext_repeated_enum_ext);
+  ASSERT_NE(dst_arr, nullptr);
+  ASSERT_EQ(2, upb_Array_Size(dst_arr));
+  EXPECT_EQ(1, upb_Array_Get(dst_arr, 0).int32_val);
+  EXPECT_EQ(2, upb_Array_Get(dst_arr, 1).int32_val);
+
+  // Unknowns should have 99.
+  upb_MessageUnknown unknown;
+  uintptr_t iter = kUpb_Message_UnknownBegin;
+  ASSERT_TRUE(upb_Message_NextUnknown2(converted, &unknown, &iter));
+  EXPECT_EQ(unknown.type, kUpb_MessageUnknownType_StringView);
+}
+
+TEST(ConvertTest, NonCanonicalInt32ToMap_Incompatible) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  upb_MessageValue ext_val;
+  ext_val.int32_val = 42;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_field_int32_ext, &ext_val,
+                           arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  const upb_MiniTable* dst_mt =
+      &upb__test__convert__MessageWithMapAt1000_msg_init;
+
+  // Conversion should FAIL (return nullptr) due to mode mismatch.
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, dst_mt, nullptr, 0, 0, arena.ptr());
+  EXPECT_EQ(converted, nullptr);
+}
+
+TEST(ConvertTest, NonCanonicalMixedToCanonical) {
+  upb::Arena arena;
+  upb_test_convert_MessageWithExtension* msg =
+      upb_test_convert_MessageWithExtension_new(arena.ptr());
+
+  // Set int32 extension (1000).
+  upb_MessageValue val_int32;
+  val_int32.int32_val = 42;
+  upb_Message_SetExtension(UPB_UPCAST(msg),
+                           upb_test_convert_ext_field_int32_ext, &val_int32,
+                           arena.ptr());
+
+  // Set string extension (1004).
+  upb_MessageValue val_str;
+  val_str.str_val = upb_StringView_FromDataAndSize("hello", 5);
+  upb_Message_SetExtension(UPB_UPCAST(msg), upb_test_convert_ext_string_ext,
+                           &val_str, arena.ptr());
+
+  const upb_MiniTable* src_mt =
+      &upb__test__convert__MessageWithExtension_msg_init;
+  const upb_MiniTable* empty_mt = &upb_0test__EmptyMessage_msg_init;
+
+  const upb_Message* non_canonical_src = upb_Message_Convert(
+      UPB_UPCAST(msg), src_mt, empty_mt, nullptr, 0, 0, arena.ptr());
+  ASSERT_NE(non_canonical_src, nullptr);
+
+  upb_ExtensionRegistry* extreg = upb_ExtensionRegistry_New(arena.ptr());
+  ASSERT_NE(extreg, nullptr);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_field_int32_ext);
+  upb_ExtensionRegistry_Add(extreg, upb_test_convert_ext_string_ext);
+
+  const upb_Message* converted = upb_Message_Convert(
+      non_canonical_src, empty_mt, src_mt, extreg, 0, 0, arena.ptr());
+  ASSERT_NE(converted, nullptr);
+
+  // Verify both extensions are present.
+  EXPECT_TRUE(upb_Message_HasExtension(converted,
+                                       upb_test_convert_ext_field_int32_ext));
+  EXPECT_EQ(42, upb_Message_GetExtensionInt32(
+                    converted, upb_test_convert_ext_field_int32_ext, 0));
+
+  EXPECT_TRUE(
+      upb_Message_HasExtension(converted, upb_test_convert_ext_string_ext));
+
+  upb_StringView dst_str =
+      upb_Message_GetExtensionString(converted, upb_test_convert_ext_string_ext,
+                                     upb_StringView_FromDataAndSize("", 0));
+  EXPECT_EQ(std::string("hello"), std::string(dst_str.data, dst_str.size));
+}
diff --git a/upb/message/convert_test.proto b/upb/message/convert_test.proto
index d2f0430..605ee19 100644
--- a/upb/message/convert_test.proto
+++ b/upb/message/convert_test.proto
@@ -12,6 +12,9 @@
 extend MessageWithExtension {
   optional int32 ext_field_int32 = 1000;
   repeated MessageWithInt32 ext_field_repeated_msg = 1001;
+  repeated int32 ext_repeated_int32 = 1003;
+  optional string ext_string = 1004;
+  repeated string ext_repeated_string = 1005;
 }
 
 message MessageWithMapMessage {
@@ -26,6 +29,18 @@
   optional int32 known_field_int32 = 1000;
 }
 
+message MessageWithKnownRepeatedInt32 {
+  repeated int32 known_repeated_int32 = 1003;
+}
+
+message MessageWithKnownString {
+  optional string known_string = 1004;
+}
+
+message MessageWithKnownRepeatedString {
+  repeated string known_repeated_string = 1005;
+}
+
 message MessageWithKnownInt64 {
   optional int64 known_field_int64 = 1000;
 }
@@ -112,6 +127,7 @@
 
 extend AnotherMessageWithExtension {
   optional int32 another_ext_field_int32 = 1000;
+  optional MessageWithInt32Clone another_ext_field_msg = 1002;
 }
 
 message SrcWithOneof {