encoding/jsonpb: add unmarshal option to ignore unknown fields

This feature seems to be used quite a bit, and the conformance tests
treat this as required, perhaps as a "required option" since the
developer guide states:

"Proto3 JSON parser should reject unknown fields by default but may
provide an option to ignore unknown fields in parsing."

Also, all invalid UTF-8 errors in skipped values are also returned as it
is similar to a parse error, except it is a non-fatal one.

Change-Id: Ia26e9a355daecdbf99af23f3061353fffa32d47d
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/174017
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/encoding/jsonpb/decode.go b/encoding/jsonpb/decode.go
index 4b0d9bb..0a08e4f 100644
--- a/encoding/jsonpb/decode.go
+++ b/encoding/jsonpb/decode.go
@@ -29,11 +29,13 @@
 type UnmarshalOptions struct {
 	pragma.NoUnkeyedLiterals
 
-	// AllowPartial accepts input for messages that will result in missing
-	// required fields. If AllowPartial is false (the default), Unmarshal will
-	// return error if there are any missing required fields.
+	// If AllowPartial is set, input for messages that will result in missing
+	// required fields will not return an error.
 	AllowPartial bool
 
+	// If DiscardUnknown is set, unknown fields are ignored.
+	DiscardUnknown bool
+
 	// Resolver is the registry used for type lookups when unmarshaling extensions
 	// and processing Any. If Resolver is not set, unmarshaling will default to
 	// using protoregistry.GlobalTypes.
@@ -217,7 +219,12 @@
 
 		if fd == nil {
 			// Field is unknown.
-			// TODO: Provide option to ignore unknown message fields.
+			if o.DiscardUnknown {
+				if err := skipJSONValue(o.decoder); !nerr.Merge(err) {
+					return err
+				}
+				continue
+			}
 			return newError("%v contains unknown field %s", msgType.FullName(), jval)
 		}
 
diff --git a/encoding/jsonpb/decode_test.go b/encoding/jsonpb/decode_test.go
index 6226b07..8f92209 100644
--- a/encoding/jsonpb/decode_test.go
+++ b/encoding/jsonpb/decode_test.go
@@ -2008,18 +2008,14 @@
 			Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
 		},
 		inputMessage: &knownpb.Any{},
-		inputText: `{
-  "@type": "foo/pb2.Nested"
-}`,
-		wantMessage: &knownpb.Any{TypeUrl: "foo/pb2.Nested"},
+		inputText:    `{"@type": "foo/pb2.Nested"}`,
+		wantMessage:  &knownpb.Any{TypeUrl: "foo/pb2.Nested"},
 	}, {
 		desc:         "Any without registered type",
 		umo:          jsonpb.UnmarshalOptions{Resolver: preg.NewTypes()},
 		inputMessage: &knownpb.Any{},
-		inputText: `{
-  "@type": "foo/pb2.Nested"
-}`,
-		wantErr: true,
+		inputText:    `{"@type": "foo/pb2.Nested"}`,
+		wantErr:      true,
 	}, {
 		desc: "Any with missing required error",
 		umo: jsonpb.UnmarshalOptions{
@@ -2129,17 +2125,9 @@
   "value": {},
   "@type": "type.googleapis.com/google.protobuf.Empty"
 }`,
-		wantMessage: func() proto.Message {
-			m := &knownpb.Empty{}
-			b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
-			if err != nil {
-				t.Fatalf("error in binary marshaling message for Any.value: %v", err)
-			}
-			return &knownpb.Any{
-				TypeUrl: "type.googleapis.com/google.protobuf.Empty",
-				Value:   b,
-			}
-		}(),
+		wantMessage: &knownpb.Any{
+			TypeUrl: "type.googleapis.com/google.protobuf.Empty",
+		},
 	}, {
 		desc: "Any with missing Empty",
 		umo: jsonpb.UnmarshalOptions{
@@ -2499,6 +2487,109 @@
 				Paths: []string{"foo_bar", "bar_foo"},
 			},
 		},
+	}, {
+		desc:         "DiscardUnknown: regular messages",
+		umo:          jsonpb.UnmarshalOptions{DiscardUnknown: true},
+		inputMessage: &pb3.Nests{},
+		inputText: `{
+  "sNested": {
+    "unknown": {
+      "foo": 1,
+	  "bar": [1, 2, 3]
+    }
+  },
+  "unknown": "not known"
+}`,
+		wantMessage: &pb3.Nests{SNested: &pb3.Nested{}},
+	}, {
+		desc:         "DiscardUnknown: repeated",
+		umo:          jsonpb.UnmarshalOptions{DiscardUnknown: true},
+		inputMessage: &pb2.Nests{},
+		inputText: `{
+  "rptNested": [
+    {"unknown": "blah"},
+	{"optString": "hello"}
+  ]
+}`,
+		wantMessage: &pb2.Nests{
+			RptNested: []*pb2.Nested{
+				{},
+				{OptString: scalar.String("hello")},
+			},
+		},
+	}, {
+		desc:         "DiscardUnknown: map",
+		umo:          jsonpb.UnmarshalOptions{DiscardUnknown: true},
+		inputMessage: &pb3.Maps{},
+		inputText: `{
+  "strToNested": {
+    "nested_one": {
+	  "unknown": "what you see is not"
+    }
+  }
+}`,
+		wantMessage: &pb3.Maps{
+			StrToNested: map[string]*pb3.Nested{
+				"nested_one": {},
+			},
+		},
+	}, {
+		desc:         "DiscardUnknown: extension",
+		umo:          jsonpb.UnmarshalOptions{DiscardUnknown: true},
+		inputMessage: &pb2.Extensions{},
+		inputText: `{
+  "[pb2.opt_ext_nested]": {
+	"unknown": []
+  }
+}`,
+		wantMessage: func() proto.Message {
+			m := &pb2.Extensions{}
+			setExtension(m, pb2.E_OptExtNested, &pb2.Nested{})
+			return m
+		}(),
+	}, {
+		desc:         "DiscardUnknown: Empty",
+		umo:          jsonpb.UnmarshalOptions{DiscardUnknown: true},
+		inputMessage: &knownpb.Empty{},
+		inputText:    `{"unknown": "something"}`,
+		wantMessage:  &knownpb.Empty{},
+	}, {
+		desc:         "DiscardUnknown: Any without type",
+		umo:          jsonpb.UnmarshalOptions{DiscardUnknown: true},
+		inputMessage: &knownpb.Any{},
+		inputText: `{
+  "value": {"foo": "bar"},
+  "unknown": true
+}`,
+		wantMessage: &knownpb.Any{},
+	}, {
+		desc: "DiscardUnknown: Any",
+		umo: jsonpb.UnmarshalOptions{
+			DiscardUnknown: true,
+			Resolver:       preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+		},
+		inputMessage: &knownpb.Any{},
+		inputText: `{
+  "@type": "foo/pb2.Nested",
+  "unknown": "none"
+}`,
+		wantMessage: &knownpb.Any{
+			TypeUrl: "foo/pb2.Nested",
+		},
+	}, {
+		desc: "DiscardUnknown: Any with Empty",
+		umo: jsonpb.UnmarshalOptions{
+			DiscardUnknown: true,
+			Resolver:       preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
+		},
+		inputMessage: &knownpb.Any{},
+		inputText: `{
+  "@type": "type.googleapis.com/google.protobuf.Empty",
+  "value": {"unknown": 47}
+}`,
+		wantMessage: &knownpb.Any{
+			TypeUrl: "type.googleapis.com/google.protobuf.Empty",
+		},
 	}}
 
 	for _, tt := range tests {
diff --git a/encoding/jsonpb/well_known_types.go b/encoding/jsonpb/well_known_types.go
index 19c8230..f974a0b 100644
--- a/encoding/jsonpb/well_known_types.go
+++ b/encoding/jsonpb/well_known_types.go
@@ -219,6 +219,10 @@
 		o.decoder.Read() // Read json.EndObject.
 		return nil
 	}
+	if o.DiscardUnknown && err == errMissingType {
+		// Treat all fields as unknowns, similar to an empty object.
+		return skipJSONValue(o.decoder)
+	}
 	var nerr errors.NonFatal
 	if !nerr.Merge(err) {
 		return errors.New("google.protobuf.Any: %v", err)
@@ -260,6 +264,7 @@
 }
 
 var errEmptyObject = errors.New(`empty object`)
+var errMissingType = errors.New(`missing "@type" field`)
 
 // findTypeURL returns the "@type" field value from the given JSON bytes. It is
 // expected that the given bytes start with json.StartObject. It returns
@@ -284,7 +289,7 @@
 			if typeURL == "" {
 				// Did not find @type field.
 				if numFields > 0 {
-					return "", errors.New(`missing "@type" field`)
+					return "", errMissingType
 				}
 				return "", errEmptyObject
 			}
@@ -298,7 +303,7 @@
 			}
 			if name != "@type" {
 				// Skip value.
-				if err := skipJSONValue(dec); err != nil {
+				if err := skipJSONValue(dec); !nerr.Merge(err) {
 					return "", err
 				}
 				continue
@@ -331,7 +336,6 @@
 // JSON value. It relies on Decoder.Read returning an error if the types are
 // not in valid sequence.
 func skipJSONValue(dec *json.Decoder) error {
-	// Ignore non-fatal errors, do not return nerr.E.
 	var nerr errors.NonFatal
 	jval, err := dec.Read()
 	if !nerr.Merge(err) {
@@ -350,7 +354,7 @@
 				return nil
 			case json.Name:
 				// Skip object field value.
-				if err := skipJSONValue(dec); err != nil {
+				if err := skipJSONValue(dec); !nerr.Merge(err) {
 					return err
 				}
 			}
@@ -367,13 +371,13 @@
 				return err
 			default:
 				// Skip array item.
-				if err := skipJSONValue(dec); err != nil {
+				if err := skipJSONValue(dec); !nerr.Merge(err) {
 					return err
 				}
 			}
 		}
 	}
-	return nil
+	return nerr.E
 }
 
 // unmarshalAnyValue unmarshals the given custom-type message from the JSON
@@ -403,6 +407,12 @@
 			}
 			switch name {
 			default:
+				if o.DiscardUnknown {
+					if err := skipJSONValue(o.decoder); !nerr.Merge(err) {
+						return err
+					}
+					continue
+				}
 				return errors.New("unknown field %q", name)
 
 			case "@type":
@@ -463,6 +473,7 @@
 }
 
 func (o UnmarshalOptions) unmarshalEmpty(pref.Message) error {
+	var nerr errors.NonFatal
 	jval, err := o.decoder.Read()
 	if err != nil {
 		return err
@@ -470,14 +481,30 @@
 	if jval.Type() != json.StartObject {
 		return unexpectedJSONError{jval}
 	}
-	jval, err = o.decoder.Read()
-	if err != nil {
-		return err
+
+	for {
+		jval, err := o.decoder.Read()
+		if !nerr.Merge(err) {
+			return err
+		}
+		switch jval.Type() {
+		case json.EndObject:
+			return nerr.E
+
+		case json.Name:
+			if o.DiscardUnknown {
+				if err := skipJSONValue(o.decoder); !nerr.Merge(err) {
+					return err
+				}
+				continue
+			}
+			name, _ := jval.Name()
+			return errors.New("unknown field %q", name)
+
+		default:
+			return unexpectedJSONError{jval}
+		}
 	}
-	if jval.Type() != json.EndObject {
-		return unexpectedJSONError{jval}
-	}
-	return nil
 }
 
 // The JSON representation for Struct is a JSON object that contains the encoded
diff --git a/internal/cmd/conformance/failure_list_go.txt b/internal/cmd/conformance/failure_list_go.txt
index f30c4c6..92dc374 100644
--- a/internal/cmd/conformance/failure_list_go.txt
+++ b/internal/cmd/conformance/failure_list_go.txt
@@ -1,7 +1 @@
-Required.Proto3.JsonInput.IgnoreUnknownJsonFalse.ProtobufOutput
-Required.Proto3.JsonInput.IgnoreUnknownJsonNull.ProtobufOutput
-Required.Proto3.JsonInput.IgnoreUnknownJsonNumber.ProtobufOutput
-Required.Proto3.JsonInput.IgnoreUnknownJsonObject.ProtobufOutput
-Required.Proto3.JsonInput.IgnoreUnknownJsonString.ProtobufOutput
-Required.Proto3.JsonInput.IgnoreUnknownJsonTrue.ProtobufOutput
 Recommended.Proto3.JsonInput.FieldMaskInvalidCharacter
diff --git a/internal/cmd/conformance/main.go b/internal/cmd/conformance/main.go
index a779929..9f1abb7 100644
--- a/internal/cmd/conformance/main.go
+++ b/internal/cmd/conformance/main.go
@@ -69,7 +69,9 @@
 	case *pb.ConformanceRequest_ProtobufPayload:
 		err = proto.Unmarshal(p.ProtobufPayload, msg)
 	case *pb.ConformanceRequest_JsonPayload:
-		err = jsonpb.Unmarshal(msg, []byte(p.JsonPayload))
+		err = jsonpb.UnmarshalOptions{
+			DiscardUnknown: req.TestCategory == pb.TestCategory_JSON_IGNORE_UNKNOWN_PARSING_TEST,
+		}.Unmarshal(msg, []byte(p.JsonPayload))
 	default:
 		return &pb.ConformanceResponse{
 			Result: &pb.ConformanceResponse_RuntimeError{