[ObjC] Start of the internal api for `GPBUnknownFields`

Add in the support for pushing the data back onto a `GPBMessage`.

PiperOrigin-RevId: 649125923
diff --git a/objectivec/BUILD.bazel b/objectivec/BUILD.bazel
index 7f154ff..11dd630 100644
--- a/objectivec/BUILD.bazel
+++ b/objectivec/BUILD.bazel
@@ -59,6 +59,10 @@
 
 objc_library(
     name = "objectivec",
+    srcs = [
+        # Private headers that aren't used from the generated sources.
+        "GPBUnknownFields_PackagePrivate.h",
+    ],
     hdrs = [
         "GPBAny.pbobjc.h",
         "GPBApi.pbobjc.h",
diff --git a/objectivec/GPBMessage.h b/objectivec/GPBMessage.h
index 12908eb..e988992 100644
--- a/objectivec/GPBMessage.h
+++ b/objectivec/GPBMessage.h
@@ -16,6 +16,7 @@
 @class GPBExtensionDescriptor;
 @class GPBFieldDescriptor;
 @class GPBUnknownFieldSet;
+@class GPBUnknownFields;
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -502,6 +503,18 @@
  **/
 - (void)clearUnknownFields;
 
+/**
+ * Merges in the data from an `GPBUnknownFields`, meaning the data from the unknown fields gets
+ * re-parsed so any known fields will be propertly set.
+ *
+ * If the intent is to replace the message's unknown fields, call `-clearUnknownFields` first.
+ *
+ * @param unknownFields     The unknown fields to merge the data from.
+ * @param extensionRegistry The extension registry to use to look up extensions, can be `nil`.
+ **/
+- (void)mergeUnknownFields:(GPBUnknownFields *)unknownFields
+         extensionRegistry:(nullable id<GPBExtensionRegistry>)extensionRegistry;
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/objectivec/GPBMessage.m b/objectivec/GPBMessage.m
index 516f0d7..b2c3ba7 100644
--- a/objectivec/GPBMessage.m
+++ b/objectivec/GPBMessage.m
@@ -22,6 +22,7 @@
 #import "GPBExtensionRegistry.h"
 #import "GPBRootObject_PackagePrivate.h"
 #import "GPBUnknownFieldSet_PackagePrivate.h"
+#import "GPBUnknownFields_PackagePrivate.h"
 #import "GPBUtilities_PackagePrivate.h"
 
 // Returns a new instance that was automatically created by |autocreator| for
@@ -1255,6 +1256,16 @@
   self.unknownFields = nil;
 }
 
+- (void)mergeUnknownFields:(GPBUnknownFields *)unknownFields
+         extensionRegistry:(nullable id<GPBExtensionRegistry>)extensionRegistry {
+  NSData *data = [unknownFields serializeAsData];
+  if (![self mergeFromData:data extensionRegistry:extensionRegistry error:NULL]) {
+#if defined(DEBUG) && DEBUG
+    NSAssert(0, @"Internal error within the library, failed to parse data from unknown fields.");
+#endif
+  };
+}
+
 - (BOOL)isInitialized {
   GPBDescriptor *descriptor = [self descriptor];
   for (GPBFieldDescriptor *field in descriptor->fields_) {
diff --git a/objectivec/GPBUnknownField.m b/objectivec/GPBUnknownField.m
index 767bc5b..f98de4c 100644
--- a/objectivec/GPBUnknownField.m
+++ b/objectivec/GPBUnknownField.m
@@ -7,10 +7,12 @@
 
 #import "GPBUnknownField.h"
 #import "GPBUnknownField_PackagePrivate.h"
+#import "GPBWireFormat.h"
 
 #import "GPBArray.h"
 #import "GPBCodedOutputStream_PackagePrivate.h"
 #import "GPBUnknownFieldSet.h"
+#import "GPBUnknownFields_PackagePrivate.h"
 
 #define ASSERT_FIELD_TYPE(type)                               \
   if (type_ != type) {                                        \
@@ -287,57 +289,90 @@
 }
 
 - (void)writeToOutput:(GPBCodedOutputStream *)output {
-  ASSERT_FIELD_TYPE(GPBUnknownFieldTypeLegacy);
-  NSUInteger count = storage_.legacy.mutableVarintList.count;
-  if (count > 0) {
-    [output writeUInt64Array:number_ values:storage_.legacy.mutableVarintList tag:0];
-  }
-  count = storage_.legacy.mutableFixed32List.count;
-  if (count > 0) {
-    [output writeFixed32Array:number_ values:storage_.legacy.mutableFixed32List tag:0];
-  }
-  count = storage_.legacy.mutableFixed64List.count;
-  if (count > 0) {
-    [output writeFixed64Array:number_ values:storage_.legacy.mutableFixed64List tag:0];
-  }
-  count = storage_.legacy.mutableLengthDelimitedList.count;
-  if (count > 0) {
-    [output writeBytesArray:number_ values:storage_.legacy.mutableLengthDelimitedList];
-  }
-  count = storage_.legacy.mutableGroupList.count;
-  if (count > 0) {
-    [output writeUnknownGroupArray:number_ values:storage_.legacy.mutableGroupList];
+  switch (type_) {
+    case GPBUnknownFieldTypeVarint:
+      [output writeUInt64:number_ value:storage_.intValue];
+      break;
+    case GPBUnknownFieldTypeFixed32:
+      [output writeFixed32:number_ value:(uint32_t)storage_.intValue];
+      break;
+    case GPBUnknownFieldTypeFixed64:
+      [output writeFixed64:number_ value:storage_.intValue];
+      break;
+    case GPBUnknownFieldTypeLengthDelimited:
+      [output writeBytes:number_ value:storage_.lengthDelimited];
+      break;
+    case GPBUnknownFieldTypeGroup:
+      [output writeRawVarint32:GPBWireFormatMakeTag(number_, GPBWireFormatStartGroup)];
+      [storage_.group writeToCodedOutputStream:output];
+      [output writeRawVarint32:GPBWireFormatMakeTag(number_, GPBWireFormatEndGroup)];
+      break;
+    case GPBUnknownFieldTypeLegacy: {
+      NSUInteger count = storage_.legacy.mutableVarintList.count;
+      if (count > 0) {
+        [output writeUInt64Array:number_ values:storage_.legacy.mutableVarintList tag:0];
+      }
+      count = storage_.legacy.mutableFixed32List.count;
+      if (count > 0) {
+        [output writeFixed32Array:number_ values:storage_.legacy.mutableFixed32List tag:0];
+      }
+      count = storage_.legacy.mutableFixed64List.count;
+      if (count > 0) {
+        [output writeFixed64Array:number_ values:storage_.legacy.mutableFixed64List tag:0];
+      }
+      count = storage_.legacy.mutableLengthDelimitedList.count;
+      if (count > 0) {
+        [output writeBytesArray:number_ values:storage_.legacy.mutableLengthDelimitedList];
+      }
+      count = storage_.legacy.mutableGroupList.count;
+      if (count > 0) {
+        [output writeUnknownGroupArray:number_ values:storage_.legacy.mutableGroupList];
+      }
+    }
   }
 }
 
 - (size_t)serializedSize {
-  ASSERT_FIELD_TYPE(GPBUnknownFieldTypeLegacy);
-  __block size_t result = 0;
-  int32_t number = number_;
-  [storage_.legacy.mutableVarintList
-      enumerateValuesWithBlock:^(uint64_t value, __unused NSUInteger idx, __unused BOOL *stop) {
-        result += GPBComputeUInt64Size(number, value);
-      }];
+  switch (type_) {
+    case GPBUnknownFieldTypeVarint:
+      return GPBComputeUInt64Size(number_, storage_.intValue);
+    case GPBUnknownFieldTypeFixed32:
+      return GPBComputeFixed32Size(number_, (uint32_t)storage_.intValue);
+    case GPBUnknownFieldTypeFixed64:
+      return GPBComputeFixed64Size(number_, storage_.intValue);
+    case GPBUnknownFieldTypeLengthDelimited:
+      return GPBComputeBytesSize(number_, storage_.lengthDelimited);
+    case GPBUnknownFieldTypeGroup:
+      return (GPBComputeTagSize(number_) * 2) + [storage_.group serializedSize];
+    case GPBUnknownFieldTypeLegacy: {
+      __block size_t result = 0;
+      int32_t number = number_;
+      [storage_.legacy.mutableVarintList
+          enumerateValuesWithBlock:^(uint64_t value, __unused NSUInteger idx, __unused BOOL *stop) {
+            result += GPBComputeUInt64Size(number, value);
+          }];
 
-  [storage_.legacy.mutableFixed32List
-      enumerateValuesWithBlock:^(uint32_t value, __unused NSUInteger idx, __unused BOOL *stop) {
-        result += GPBComputeFixed32Size(number, value);
-      }];
+      [storage_.legacy.mutableFixed32List
+          enumerateValuesWithBlock:^(uint32_t value, __unused NSUInteger idx, __unused BOOL *stop) {
+            result += GPBComputeFixed32Size(number, value);
+          }];
 
-  [storage_.legacy.mutableFixed64List
-      enumerateValuesWithBlock:^(uint64_t value, __unused NSUInteger idx, __unused BOOL *stop) {
-        result += GPBComputeFixed64Size(number, value);
-      }];
+      [storage_.legacy.mutableFixed64List
+          enumerateValuesWithBlock:^(uint64_t value, __unused NSUInteger idx, __unused BOOL *stop) {
+            result += GPBComputeFixed64Size(number, value);
+          }];
 
-  for (NSData *data in storage_.legacy.mutableLengthDelimitedList) {
-    result += GPBComputeBytesSize(number, data);
+      for (NSData *data in storage_.legacy.mutableLengthDelimitedList) {
+        result += GPBComputeBytesSize(number, data);
+      }
+
+      for (GPBUnknownFieldSet *set in storage_.legacy.mutableGroupList) {
+        result += GPBComputeUnknownGroupSize(number, set);
+      }
+
+      return result;
+    }
   }
-
-  for (GPBUnknownFieldSet *set in storage_.legacy.mutableGroupList) {
-    result += GPBComputeUnknownGroupSize(number, set);
-  }
-
-  return result;
 }
 
 - (void)writeAsMessageSetExtensionToOutput:(GPBCodedOutputStream *)output {
diff --git a/objectivec/GPBUnknownFields.m b/objectivec/GPBUnknownFields.m
index aed1bc9..8856ac5 100644
--- a/objectivec/GPBUnknownFields.m
+++ b/objectivec/GPBUnknownFields.m
@@ -6,6 +6,8 @@
 // https://developers.google.com/open-source/licenses/bsd
 
 #import "GPBUnknownFields.h"
+#import "GPBCodedOutputStream.h"
+#import "GPBCodedOutputStream_PackagePrivate.h"
 #import "GPBUnknownField_PackagePrivate.h"
 
 #define CHECK_FIELD_NUMBER(number)                                                      \
@@ -137,6 +139,44 @@
   return [fields_ countByEnumeratingWithState:state objects:stackbuf count:len];
 }
 
+#pragma mark - Internal Methods
+
+- (size_t)serializedSize {
+  size_t result = 0;
+  for (GPBUnknownField *field in self->fields_) {
+    result += [field serializedSize];
+  }
+  return result;
+}
+
+- (void)writeToCodedOutputStream:(GPBCodedOutputStream *)output {
+  for (GPBUnknownField *field in fields_) {
+    [field writeToOutput:output];
+  }
+}
+
+- (NSData *)serializeAsData {
+  if (fields_.count == 0) {
+    return [NSData data];
+  }
+  size_t expectedSize = [self serializedSize];
+  NSMutableData *data = [NSMutableData dataWithLength:expectedSize];
+  GPBCodedOutputStream *stream = [[GPBCodedOutputStream alloc] initWithData:data];
+  @try {
+    [self writeToCodedOutputStream:stream];
+    [stream flush];
+  } @catch (NSException *exception) {
+#if defined(DEBUG) && DEBUG
+    NSLog(@"Internal exception while building GPBUnknownFields serialized data: %@", exception);
+#endif
+  }
+#if defined(DEBUG) && DEBUG
+  NSAssert([stream bytesWritten] == expectedSize, @"Internal error within the library");
+#endif
+  [stream release];
+  return data;
+}
+
 @end
 
 @implementation GPBUnknownFields (AccessHelpers)
diff --git a/objectivec/GPBUnknownFields_PackagePrivate.h b/objectivec/GPBUnknownFields_PackagePrivate.h
new file mode 100644
index 0000000..dc63d3a
--- /dev/null
+++ b/objectivec/GPBUnknownFields_PackagePrivate.h
@@ -0,0 +1,20 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2024 Google Inc.  All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+#import <Foundation/Foundation.h>
+
+#import "GPBUnknownFields.h"
+
+@class GPBCodedOutputStream;
+
+@interface GPBUnknownFields ()
+
+- (size_t)serializedSize;
+- (nonnull NSData *)serializeAsData;
+- (void)writeToCodedOutputStream:(nonnull GPBCodedOutputStream *)output;
+
+@end
diff --git a/objectivec/ProtocolBuffers_OSX.xcodeproj/project.pbxproj b/objectivec/ProtocolBuffers_OSX.xcodeproj/project.pbxproj
index 32bfb22..850e26b 100644
--- a/objectivec/ProtocolBuffers_OSX.xcodeproj/project.pbxproj
+++ b/objectivec/ProtocolBuffers_OSX.xcodeproj/project.pbxproj
@@ -76,6 +76,7 @@
 		F43ADD432C2F381F005312E5 /* GPBCompileTest25.m in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD422C2F381F005312E5 /* GPBCompileTest25.m */; };
 		F43ADD502C333D6C005312E5 /* GPBUnknownFields+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD4F2C333D6B005312E5 /* GPBUnknownFields+Additions.swift */; };
 		F43ADD562C345CED005312E5 /* GPBUnknownField+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD552C345CED005312E5 /* GPBUnknownField+Additions.swift */; };
+		F43ADD5C2C35A6CC005312E5 /* GPBUnknownFields_PackagePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F43ADD5B2C35A6CC005312E5 /* GPBUnknownFields_PackagePrivate.h */; };
 		F43C88D0191D77FC009E917D /* text_format_unittest_data.txt in Resources */ = {isa = PBXBuildFile; fileRef = F43C88CF191D77FC009E917D /* text_format_unittest_data.txt */; };
 		F4487C4D1A9F8E0200531423 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; };
 		F4487C521A9F8E4D00531423 /* GPBProtocolBuffers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BCF338814ED799900BC5317 /* GPBProtocolBuffers.m */; };
@@ -218,6 +219,7 @@
 		F43ADD422C2F381F005312E5 /* GPBCompileTest25.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPBCompileTest25.m; sourceTree = "<group>"; };
 		F43ADD4F2C333D6B005312E5 /* GPBUnknownFields+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPBUnknownFields+Additions.swift"; sourceTree = "<group>"; };
 		F43ADD552C345CED005312E5 /* GPBUnknownField+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPBUnknownField+Additions.swift"; sourceTree = "<group>"; };
+		F43ADD5B2C35A6CC005312E5 /* GPBUnknownFields_PackagePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPBUnknownFields_PackagePrivate.h; sourceTree = "<group>"; };
 		F43C88CF191D77FC009E917D /* text_format_unittest_data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = text_format_unittest_data.txt; sourceTree = "<group>"; };
 		F4411BE71AF12FD700324B4A /* GPBArray_PackagePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GPBArray_PackagePrivate.h; sourceTree = "<group>"; };
 		F4487C511A9F8E0200531423 /* libTestSingleSourceBuild.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTestSingleSourceBuild.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -422,6 +424,7 @@
 				7461B4AE0F94F99000A0C422 /* GPBUnknownField.h */,
 				7461B4AF0F94F99000A0C422 /* GPBUnknownField.m */,
 				F43ADD552C345CED005312E5 /* GPBUnknownField+Additions.swift */,
+				F43ADD5B2C35A6CC005312E5 /* GPBUnknownFields_PackagePrivate.h */,
 				F43ADD2F2C2F2B91005312E5 /* GPBUnknownFields.h */,
 				F43ADD2E2C2F2B91005312E5 /* GPBUnknownFields.m */,
 				F43ADD4F2C333D6B005312E5 /* GPBUnknownFields+Additions.swift */,
@@ -582,6 +585,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				F43ADD312C2F2B91005312E5 /* GPBUnknownFields.h in Headers */,
+				F43ADD5C2C35A6CC005312E5 /* GPBUnknownFields_PackagePrivate.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/objectivec/ProtocolBuffers_iOS.xcodeproj/project.pbxproj b/objectivec/ProtocolBuffers_iOS.xcodeproj/project.pbxproj
index a01147a..63516bf 100644
--- a/objectivec/ProtocolBuffers_iOS.xcodeproj/project.pbxproj
+++ b/objectivec/ProtocolBuffers_iOS.xcodeproj/project.pbxproj
@@ -75,6 +75,7 @@
 		F43ADD472C2F387A005312E5 /* GPBCompileTest25.m in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD462C2F387A005312E5 /* GPBCompileTest25.m */; };
 		F43ADD522C333E58005312E5 /* GPBUnknownFields+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD512C333E58005312E5 /* GPBUnknownFields+Additions.swift */; };
 		F43ADD582C345D0D005312E5 /* GPBUnknownField+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD572C345D0D005312E5 /* GPBUnknownField+Additions.swift */; };
+		F43ADD5E2C35A6E1005312E5 /* GPBUnknownFields_PackagePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F43ADD5D2C35A6E1005312E5 /* GPBUnknownFields_PackagePrivate.h */; };
 		F43C88D0191D77FC009E917D /* text_format_unittest_data.txt in Resources */ = {isa = PBXBuildFile; fileRef = F43C88CF191D77FC009E917D /* text_format_unittest_data.txt */; };
 		F4487C6A1A9F8F8100531423 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; };
 		F4487C6F1A9F8FFF00531423 /* GPBProtocolBuffers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BCF338814ED799900BC5317 /* GPBProtocolBuffers.m */; };
@@ -218,6 +219,7 @@
 		F43ADD462C2F387A005312E5 /* GPBCompileTest25.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPBCompileTest25.m; sourceTree = "<group>"; };
 		F43ADD512C333E58005312E5 /* GPBUnknownFields+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPBUnknownFields+Additions.swift"; sourceTree = "<group>"; };
 		F43ADD572C345D0D005312E5 /* GPBUnknownField+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPBUnknownField+Additions.swift"; sourceTree = "<group>"; };
+		F43ADD5D2C35A6E1005312E5 /* GPBUnknownFields_PackagePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPBUnknownFields_PackagePrivate.h; sourceTree = "<group>"; };
 		F43C88CF191D77FC009E917D /* text_format_unittest_data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = text_format_unittest_data.txt; sourceTree = "<group>"; };
 		F4411BE81AF1301700324B4A /* GPBArray_PackagePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GPBArray_PackagePrivate.h; sourceTree = "<group>"; };
 		F4487C6E1A9F8F8100531423 /* libTestSingleSourceBuild.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTestSingleSourceBuild.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -427,6 +429,7 @@
 				7461B4AE0F94F99000A0C422 /* GPBUnknownField.h */,
 				7461B4AF0F94F99000A0C422 /* GPBUnknownField.m */,
 				F43ADD572C345D0D005312E5 /* GPBUnknownField+Additions.swift */,
+				F43ADD5D2C35A6E1005312E5 /* GPBUnknownFields_PackagePrivate.h */,
 				F43ADD372C2F2D06005312E5 /* GPBUnknownFields.h */,
 				F43ADD362C2F2D06005312E5 /* GPBUnknownFields.m */,
 				F43ADD512C333E58005312E5 /* GPBUnknownFields+Additions.swift */,
@@ -586,6 +589,7 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				F43ADD5E2C35A6E1005312E5 /* GPBUnknownFields_PackagePrivate.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/objectivec/ProtocolBuffers_tvOS.xcodeproj/project.pbxproj b/objectivec/ProtocolBuffers_tvOS.xcodeproj/project.pbxproj
index 76c4466..7910048 100644
--- a/objectivec/ProtocolBuffers_tvOS.xcodeproj/project.pbxproj
+++ b/objectivec/ProtocolBuffers_tvOS.xcodeproj/project.pbxproj
@@ -76,6 +76,7 @@
 		F43ADD492C2F389D005312E5 /* GPBCompileTest25.m in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD482C2F389D005312E5 /* GPBCompileTest25.m */; };
 		F43ADD542C333EE5005312E5 /* GPBUnknownFields+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD532C333EE5005312E5 /* GPBUnknownFields+Additions.swift */; };
 		F43ADD5A2C345D36005312E5 /* GPBUnknownField+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ADD592C345D36005312E5 /* GPBUnknownField+Additions.swift */; };
+		F43ADD602C35A6F4005312E5 /* GPBUnknownFields_PackagePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F43ADD5F2C35A6F4005312E5 /* GPBUnknownFields_PackagePrivate.h */; };
 		F43C88D0191D77FC009E917D /* text_format_unittest_data.txt in Resources */ = {isa = PBXBuildFile; fileRef = F43C88CF191D77FC009E917D /* text_format_unittest_data.txt */; };
 		F4487C6A1A9F8F8100531423 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; };
 		F4487C6F1A9F8FFF00531423 /* GPBProtocolBuffers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BCF338814ED799900BC5317 /* GPBProtocolBuffers.m */; };
@@ -219,6 +220,7 @@
 		F43ADD482C2F389D005312E5 /* GPBCompileTest25.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPBCompileTest25.m; sourceTree = "<group>"; };
 		F43ADD532C333EE5005312E5 /* GPBUnknownFields+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPBUnknownFields+Additions.swift"; sourceTree = "<group>"; };
 		F43ADD592C345D36005312E5 /* GPBUnknownField+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPBUnknownField+Additions.swift"; sourceTree = "<group>"; };
+		F43ADD5F2C35A6F4005312E5 /* GPBUnknownFields_PackagePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPBUnknownFields_PackagePrivate.h; sourceTree = "<group>"; };
 		F43C88CF191D77FC009E917D /* text_format_unittest_data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = text_format_unittest_data.txt; sourceTree = "<group>"; };
 		F4411BE81AF1301700324B4A /* GPBArray_PackagePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GPBArray_PackagePrivate.h; sourceTree = "<group>"; };
 		F4487C6E1A9F8F8100531423 /* libTestSingleSourceBuild.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTestSingleSourceBuild.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -428,6 +430,7 @@
 				7461B4AE0F94F99000A0C422 /* GPBUnknownField.h */,
 				7461B4AF0F94F99000A0C422 /* GPBUnknownField.m */,
 				F43ADD592C345D36005312E5 /* GPBUnknownField+Additions.swift */,
+				F43ADD5F2C35A6F4005312E5 /* GPBUnknownFields_PackagePrivate.h */,
 				F43ADD3C2C2F2D3D005312E5 /* GPBUnknownFields.h */,
 				F43ADD3B2C2F2D3D005312E5 /* GPBUnknownFields.m */,
 				F43ADD532C333EE5005312E5 /* GPBUnknownFields+Additions.swift */,
@@ -588,6 +591,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				F43ADD3E2C2F2D3D005312E5 /* GPBUnknownFields.h in Headers */,
+				F43ADD602C35A6F4005312E5 /* GPBUnknownFields_PackagePrivate.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/objectivec/Tests/GPBTestUtilities.h b/objectivec/Tests/GPBTestUtilities.h
index 1072f88..03ac8f9 100644
--- a/objectivec/Tests/GPBTestUtilities.h
+++ b/objectivec/Tests/GPBTestUtilities.h
@@ -20,6 +20,22 @@
   return [NSData dataWithBytes:str length:strlen(str)];
 }
 
+static inline NSData *_DataFromBytesInternal(int32_t unused, ...) {
+  NSMutableData *values = [NSMutableData dataWithCapacity:0];
+  va_list list;
+  va_start(list, unused);
+  int32_t n;
+  while ((n = va_arg(list, int32_t)) != 256) {
+    NSCAssert(n >= 0 && n < 256, @"Only 8 bit values");
+    uint8_t u = (uint8_t)n;
+    [values appendBytes:&u length:1];
+  }
+  va_end(list);
+  return values;
+}
+
+#define DataFromBytes(...) _DataFromBytesInternal(0, __VA_ARGS__, 256)
+
 // Helper for uses of C arrays in tests cases.
 #ifndef GPBARRAYSIZE
 #define GPBARRAYSIZE(a) ((sizeof(a) / sizeof((a[0]))))
diff --git a/objectivec/Tests/GPBUnknownFieldsTest.m b/objectivec/Tests/GPBUnknownFieldsTest.m
index 552c21d..01f3668 100644
--- a/objectivec/Tests/GPBUnknownFieldsTest.m
+++ b/objectivec/Tests/GPBUnknownFieldsTest.m
@@ -5,9 +5,12 @@
 // license that can be found in the LICENSE file or at
 // https://developers.google.com/open-source/licenses/bsd
 
+#import <Foundation/Foundation.h>
+
 #import "GPBTestUtilities.h"
 #import "GPBUnknownField.h"
 #import "GPBUnknownFields.h"
+#import "GPBUnknownFields_PackagePrivate.h"
 #import "objectivec/Tests/Unittest.pbobjc.h"
 
 @interface UnknownFieldsTest : GPBTestCase
@@ -511,4 +514,66 @@
   XCTAssertThrowsSpecificNamed([ufs fields:-1], NSException, NSInvalidArgumentException);
 }
 
+- (void)testSerialize {
+  // Don't need to test CodedOutputStream, just make sure things basically end up there.
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    XCTAssertEqualObjects([ufs serializeAsData], [NSData data]);
+  }
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    [ufs addFieldNumber:1 varint:1];
+    XCTAssertEqualObjects([ufs serializeAsData], DataFromBytes(0x08, 0x01));
+  }
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    [ufs addFieldNumber:1 fixed32:1];
+    XCTAssertEqualObjects([ufs serializeAsData], DataFromBytes(0x0d, 0x01, 0x00, 0x00, 0x00));
+  }
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    [ufs addFieldNumber:1 fixed64:1];
+    XCTAssertEqualObjects([ufs serializeAsData],
+                          DataFromBytes(0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
+  }
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    [ufs addFieldNumber:1 lengthDelimited:DataFromCStr("foo")];
+    XCTAssertEqualObjects([ufs serializeAsData], DataFromBytes(0x0a, 0x03, 0x66, 0x6f, 0x6f));
+  }
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    [ufs addGroupWithFieldNumber:1];  // Empty group
+    XCTAssertEqualObjects([ufs serializeAsData], DataFromBytes(0x0b, 0x0c));
+  }
+  {
+    GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+    GPBUnknownFields* group = [ufs addGroupWithFieldNumber:1];  // With some fields
+    [group addFieldNumber:10 varint:10];
+    [group addFieldNumber:11 fixed32:32];
+    [group addFieldNumber:12 fixed32:32];
+    XCTAssertEqualObjects([ufs serializeAsData],
+                          DataFromBytes(0x0b, 0x50, 0x0a, 0x5d, 0x20, 0x00, 0x00, 0x00, 0x65, 0x20,
+                                        0x00, 0x00, 0x00, 0x0c));
+  }
+}
+
+- (void)testMessageMergeUnknowns {
+  GPBUnknownFields* ufs = [[[GPBUnknownFields alloc] init] autorelease];
+  [ufs addFieldNumber:TestAllTypes_FieldNumber_OptionalInt64 varint:100];
+  [ufs addFieldNumber:TestAllTypes_FieldNumber_OptionalFixed32 fixed32:200];
+  [ufs addFieldNumber:TestAllTypes_FieldNumber_OptionalFixed64 fixed64:300];
+  [ufs addFieldNumber:TestAllTypes_FieldNumber_OptionalBytes lengthDelimited:DataFromCStr("foo")];
+  GPBUnknownFields* group = [ufs addGroupWithFieldNumber:TestAllTypes_FieldNumber_OptionalGroup];
+  [group addFieldNumber:TestAllTypes_OptionalGroup_FieldNumber_A varint:55];
+
+  TestAllTypes* msg = [TestAllTypes message];
+  [msg mergeUnknownFields:ufs extensionRegistry:nil];
+  XCTAssertEqual(msg.optionalInt64, 100);
+  XCTAssertEqual(msg.optionalFixed32, 200);
+  XCTAssertEqual(msg.optionalFixed64, 300);
+  XCTAssertEqualObjects(msg.optionalBytes, DataFromCStr("foo"));
+  XCTAssertEqual(msg.optionalGroup.a, 55);
+}
+
 @end