Repository: apple/swift-protobuf
Pull request #1172 (2021-09-14 22:22:15)

Support JSON `ignoreUnknownFields` within WKTs.


Patch:
diff --git a/Sources/SwiftProtobuf/AnyMessageStorage.swift b/Sources/SwiftProtobuf/AnyMessageStorage.swift
index 01e5b4d5..2f9be74b 100644
--- a/Sources/SwiftProtobuf/AnyMessageStorage.swift
+++ b/Sources/SwiftProtobuf/AnyMessageStorage.swift
@@ -69,17 +69,26 @@ fileprivate func unpack(contentJSON: Data,
   var value = String()
   try contentJSON.withUnsafeBytes { (body: UnsafeRawBufferPointer) in
     if body.count > 0 {
+      // contentJSON will be the valid JSON for inside an object (everything but
+      // the '{' and '}', so minimal validation is needed.
       var scanner = JSONScanner(source: body, options: options, extensions: extensions)
-      let key = try scanner.nextQuotedString()
-      if key != "value" {
-        // The only thing within a WKT should be "value".
-        throw AnyUnpackError.malformedWellKnownTypeJSON
+      while !scanner.complete {
+        let key = try scanner.nextQuotedString()
+        try scanner.skipRequiredColon()
+        if key == "value" {
+          value = try scanner.skip()
+          break
+        }
+        if !options.ignoreUnknownFields {
+          // The only thing within a WKT should be "value".
+          throw AnyUnpackError.malformedWellKnownTypeJSON
+        }
+        let _ = try scanner.skip()
+        try scanner.skipRequiredComma()
       }
-      try scanner.skipRequiredColon()  // Can't fail
-      value = try scanner.skip()
-      if !scanner.complete {
-        // If that wasn't the end, then there was another key,
-        // and WKTs should only have the one.
+      if !options.ignoreUnknownFields && !scanner.complete {
+        // If that wasn't the end, then there was another key, and WKTs should
+        // only have the one when not skipping unknowns.
         throw AnyUnpackError.malformedWellKnownTypeJSON
       }
     }


Test patch:
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index 67c78622..903c8b2a 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -269,6 +269,7 @@ extension Test_Any {
         ("test_Any_OddTypeURL_FromValue", test_Any_OddTypeURL_FromValue),
         ("test_Any_OddTypeURL_FromMessage", test_Any_OddTypeURL_FromMessage),
         ("test_Any_JSON_Extensions", test_Any_JSON_Extensions),
+        ("test_Any_WKT_UnknownFields", test_Any_WKT_UnknownFields),
         ("test_IsA", test_IsA),
         ("test_Any_Registry", test_Any_Registry)
     ]
diff --git a/Tests/SwiftProtobufTests/Test_Any.swift b/Tests/SwiftProtobufTests/Test_Any.swift
index b4dbce3b..e48af0a2 100644
--- a/Tests/SwiftProtobufTests/Test_Any.swift
+++ b/Tests/SwiftProtobufTests/Test_Any.swift
@@ -679,7 +679,40 @@ class Test_Any: XCTestCase {
       XCTAssertTrue(Google_Protobuf_Any.register(messageType: ProtobufUnittest_TestAllExtensions.self))
       // Throws because the extensions can't be implicitly transcoded
       XCTAssertThrowsError(try decodedWithExtensions.serializedData())
-  }
+    }
+
+    func test_Any_WKT_UnknownFields() throws {
+      let testcases = [
+        // unknown field before value
+        "{\"optionalAny\":{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"fred\":1,\"value\":\"99.001s\"}}",
+        // unknown field after value
+        "{\"optionalAny\":{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"99.001s\",\"fred\":1}}",
+      ]
+      for json in testcases {
+        for ignoreUnknown in [false, true] {
+          var options = JSONDecodingOptions()
+          options.ignoreUnknownFields = ignoreUnknown
+          // This may appear a little odd, since Any lazy parses, this will
+          // always succeed because the Any isn't decoded until requested.
+          let decoded = try! ProtobufTestMessages_Proto3_TestAllTypesProto3(jsonString: json, options: options)
+
+          XCTAssertNotNil(decoded.optionalAny)
+          let anyField = decoded.optionalAny
+          do {
+            let unpacked = try Google_Protobuf_Duration(unpackingAny: anyField)
+            XCTAssertTrue(ignoreUnknown)  // Should have throw if not ignoring unknowns.
+            XCTAssertEqual(unpacked.seconds, 99)
+            XCTAssertEqual(unpacked.nanos, 1000000)
+          } catch {
+            XCTAssertTrue(!ignoreUnknown)
+          }
+
+          // The extra field should still be there.
+          let encoded = try decoded.jsonString()
+          XCTAssertEqual(encoded, json)
+        }
+      }
+    }
 
     func test_IsA() {
       var msg = Google_Protobuf_Any()