encoding/jsonpb: add AllowPartial option to MarshalOptions and UnmarshalOptions

Added tests related to required fields and AllowPartial. I somehow
missed this before.

Change-Id: I3eb17347b1f3a99be3d65af06c4549abcc87ae39
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/169701
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/encoding/jsonpb/decode.go b/encoding/jsonpb/decode.go
index 221adc7..816ba0a 100644
--- a/encoding/jsonpb/decode.go
+++ b/encoding/jsonpb/decode.go
@@ -29,6 +29,11 @@
 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.
+	AllowPartial 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.
@@ -229,18 +234,20 @@
 			if err := o.unmarshalSingular(knownFields, fd); !nerr.Merge(err) {
 				return errors.New("%v|%q: %v", fd.FullName(), name, err)
 			}
-			if cardinality == pref.Required {
+			if !o.AllowPartial && cardinality == pref.Required {
 				reqNums.Set(num)
 			}
 		}
 	}
 
-	// Check for any missing required fields.
-	allReqNums := msgType.RequiredNumbers()
-	if reqNums.Len() != allReqNums.Len() {
-		for i := 0; i < allReqNums.Len(); i++ {
-			if num := allReqNums.Get(i); !reqNums.Has(uint64(num)) {
-				nerr.AppendRequiredNotSet(string(fieldDescs.ByNumber(num).FullName()))
+	if !o.AllowPartial {
+		// Check for any missing required fields.
+		allReqNums := msgType.RequiredNumbers()
+		if reqNums.Len() != allReqNums.Len() {
+			for i := 0; i < allReqNums.Len(); i++ {
+				if num := allReqNums.Get(i); !reqNums.Has(uint64(num)) {
+					nerr.AppendRequiredNotSet(string(fieldDescs.ByNumber(num).FullName()))
+				}
 			}
 		}
 	}
diff --git a/encoding/jsonpb/decode_test.go b/encoding/jsonpb/decode_test.go
index 648a3b1..a1ad766 100644
--- a/encoding/jsonpb/decode_test.go
+++ b/encoding/jsonpb/decode_test.go
@@ -986,6 +986,190 @@
 		},
 		wantErr: true,
 	}, {
+		desc:         "required fields not set",
+		inputMessage: &pb2.Requireds{},
+		wantErr:      true,
+	}, {
+		desc:         "required field set",
+		inputMessage: &pb2.PartialRequired{},
+		inputText: `{
+  "reqString": "this is required"
+}`,
+		wantMessage: &pb2.PartialRequired{
+			ReqString: scalar.String("this is required"),
+		},
+	}, {
+		desc:         "required fields partially set",
+		inputMessage: &pb2.Requireds{},
+		inputText: `{
+  "reqBool": false,
+  "reqSfixed64": 42,
+  "reqString": "hello",
+  "reqEnum": "ONE"
+}`,
+		wantMessage: &pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqSfixed64: scalar.Int64(42),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_ONE.Enum(),
+		},
+		wantErr: true,
+	}, {
+		desc:         "required fields partially set with AllowPartial",
+		umo:          jsonpb.UnmarshalOptions{AllowPartial: true},
+		inputMessage: &pb2.Requireds{},
+		inputText: `{
+  "reqBool": false,
+  "reqSfixed64": 42,
+  "reqString": "hello",
+  "reqEnum": "ONE"
+}`,
+		wantMessage: &pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqSfixed64: scalar.Int64(42),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_ONE.Enum(),
+		},
+	}, {
+		desc:         "required fields all set",
+		inputMessage: &pb2.Requireds{},
+		inputText: `{
+  "reqBool": false,
+  "reqSfixed64": 42,
+  "reqDouble": 1.23,
+  "reqString": "hello",
+  "reqEnum": "ONE",
+  "reqNested": {}
+}`,
+		wantMessage: &pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqSfixed64: scalar.Int64(42),
+			ReqDouble:   scalar.Float64(1.23),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_ONE.Enum(),
+			ReqNested:   &pb2.Nested{},
+		},
+	}, {
+		desc:         "indirect required field",
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "optNested": {}
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			OptNested: &pb2.NestedWithRequired{},
+		},
+		wantErr: true,
+	}, {
+		desc:         "indirect required field with AllowPartial",
+		umo:          jsonpb.UnmarshalOptions{AllowPartial: true},
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "optNested": {}
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			OptNested: &pb2.NestedWithRequired{},
+		},
+	}, {
+		desc:         "indirect required field in repeated",
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "rptNested": [
+    {"reqString": "one"},
+    {}
+  ]
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			RptNested: []*pb2.NestedWithRequired{
+				{
+					ReqString: scalar.String("one"),
+				},
+				{},
+			},
+		},
+		wantErr: true,
+	}, {
+		desc:         "indirect required field in repeated with AllowPartial",
+		umo:          jsonpb.UnmarshalOptions{AllowPartial: true},
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "rptNested": [
+    {"reqString": "one"},
+    {}
+  ]
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			RptNested: []*pb2.NestedWithRequired{
+				{
+					ReqString: scalar.String("one"),
+				},
+				{},
+			},
+		},
+	}, {
+		desc:         "indirect required field in map",
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "strToNested": {
+    "missing": {},
+	"contains": {
+      "reqString": "here"
+    }
+  }
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			StrToNested: map[string]*pb2.NestedWithRequired{
+				"missing": &pb2.NestedWithRequired{},
+				"contains": &pb2.NestedWithRequired{
+					ReqString: scalar.String("here"),
+				},
+			},
+		},
+		wantErr: true,
+	}, {
+		desc:         "indirect required field in map with AllowPartial",
+		umo:          jsonpb.UnmarshalOptions{AllowPartial: true},
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "strToNested": {
+    "missing": {},
+	"contains": {
+      "reqString": "here"
+    }
+  }
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			StrToNested: map[string]*pb2.NestedWithRequired{
+				"missing": &pb2.NestedWithRequired{},
+				"contains": &pb2.NestedWithRequired{
+					ReqString: scalar.String("here"),
+				},
+			},
+		},
+	}, {
+		desc:         "indirect required field in oneof",
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "oneofNested": {}
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			Union: &pb2.IndirectRequired_OneofNested{
+				OneofNested: &pb2.NestedWithRequired{},
+			},
+		},
+		wantErr: true,
+	}, {
+		desc:         "indirect required field in oneof with AllowPartial",
+		umo:          jsonpb.UnmarshalOptions{AllowPartial: true},
+		inputMessage: &pb2.IndirectRequired{},
+		inputText: `{
+  "oneofNested": {}
+}`,
+		wantMessage: &pb2.IndirectRequired{
+			Union: &pb2.IndirectRequired_OneofNested{
+				OneofNested: &pb2.NestedWithRequired{},
+			},
+		},
+	}, {
 		desc:         "extensions of non-repeated fields",
 		inputMessage: &pb2.Extensions{},
 		inputText: `{
diff --git a/encoding/jsonpb/encode.go b/encoding/jsonpb/encode.go
index b3a0a76..8e25183 100644
--- a/encoding/jsonpb/encode.go
+++ b/encoding/jsonpb/encode.go
@@ -28,6 +28,11 @@
 type MarshalOptions struct {
 	pragma.NoUnkeyedLiterals
 
+	// AllowPartial allows messages that have missing required fields to marshal
+	// without returning an error. If AllowPartial is false (the default),
+	// Marshal will return error if there are any missing required fields.
+	AllowPartial bool
+
 	// If Indent is a non-empty string, it causes entries for an Array or Object
 	// to be preceded by the indent and trailed by a newline. Indent can only be
 	// composed of space or tab characters.
@@ -90,7 +95,7 @@
 		num := fd.Number()
 
 		if !knownFields.Has(num) {
-			if fd.Cardinality() == pref.Required {
+			if !o.AllowPartial && fd.Cardinality() == pref.Required {
 				// Treat unset required fields as a non-fatal error.
 				nerr.AppendRequiredNotSet(string(fd.FullName()))
 			}
diff --git a/encoding/jsonpb/encode_test.go b/encoding/jsonpb/encode_test.go
index 09cb9f8..d1c151c 100644
--- a/encoding/jsonpb/encode_test.go
+++ b/encoding/jsonpb/encode_test.go
@@ -727,6 +727,167 @@
   }
 }`,
 	}, {
+		desc:    "required fields not set",
+		input:   &pb2.Requireds{},
+		want:    `{}`,
+		wantErr: true,
+	}, {
+		desc: "required fields partially set",
+		input: &pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqSfixed64: scalar.Int64(0),
+			ReqDouble:   scalar.Float64(1.23),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_ONE.Enum(),
+		},
+		want: `{
+  "reqBool": false,
+  "reqSfixed64": "0",
+  "reqDouble": 1.23,
+  "reqString": "hello",
+  "reqEnum": "ONE"
+}`,
+		wantErr: true,
+	}, {
+		desc: "required fields not set with AllowPartial",
+		mo:   jsonpb.MarshalOptions{AllowPartial: true},
+		input: &pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqSfixed64: scalar.Int64(0),
+			ReqDouble:   scalar.Float64(1.23),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_ONE.Enum(),
+		},
+		want: `{
+  "reqBool": false,
+  "reqSfixed64": "0",
+  "reqDouble": 1.23,
+  "reqString": "hello",
+  "reqEnum": "ONE"
+}`,
+	}, {
+		desc: "required fields all set",
+		input: &pb2.Requireds{
+			ReqBool:     scalar.Bool(false),
+			ReqSfixed64: scalar.Int64(0),
+			ReqDouble:   scalar.Float64(1.23),
+			ReqString:   scalar.String("hello"),
+			ReqEnum:     pb2.Enum_ONE.Enum(),
+			ReqNested:   &pb2.Nested{},
+		},
+		want: `{
+  "reqBool": false,
+  "reqSfixed64": "0",
+  "reqDouble": 1.23,
+  "reqString": "hello",
+  "reqEnum": "ONE",
+  "reqNested": {}
+}`,
+	}, {
+		desc: "indirect required field",
+		input: &pb2.IndirectRequired{
+			OptNested: &pb2.NestedWithRequired{},
+		},
+		want: `{
+  "optNested": {}
+}`,
+		wantErr: true,
+	}, {
+		desc: "indirect required field with AllowPartial",
+		mo:   jsonpb.MarshalOptions{AllowPartial: true},
+		input: &pb2.IndirectRequired{
+			OptNested: &pb2.NestedWithRequired{},
+		},
+		want: `{
+  "optNested": {}
+}`,
+	}, {
+		desc: "indirect required field in empty repeated",
+		input: &pb2.IndirectRequired{
+			RptNested: []*pb2.NestedWithRequired{},
+		},
+		want: `{}`,
+	}, {
+		desc: "indirect required field in repeated",
+		input: &pb2.IndirectRequired{
+			RptNested: []*pb2.NestedWithRequired{
+				&pb2.NestedWithRequired{},
+			},
+		},
+		want: `{
+  "rptNested": [
+    {}
+  ]
+}`,
+		wantErr: true,
+	}, {
+		desc: "indirect required field in repeated with AllowPartial",
+		mo:   jsonpb.MarshalOptions{AllowPartial: true},
+		input: &pb2.IndirectRequired{
+			RptNested: []*pb2.NestedWithRequired{
+				&pb2.NestedWithRequired{},
+			},
+		},
+		want: `{
+  "rptNested": [
+    {}
+  ]
+}`,
+	}, {
+		desc: "indirect required field in empty map",
+		input: &pb2.IndirectRequired{
+			StrToNested: map[string]*pb2.NestedWithRequired{},
+		},
+		want: "{}",
+	}, {
+		desc: "indirect required field in map",
+		input: &pb2.IndirectRequired{
+			StrToNested: map[string]*pb2.NestedWithRequired{
+				"fail": &pb2.NestedWithRequired{},
+			},
+		},
+		want: `{
+  "strToNested": {
+    "fail": {}
+  }
+}`,
+		wantErr: true,
+	}, {
+		desc: "indirect required field in map with AllowPartial",
+		mo:   jsonpb.MarshalOptions{AllowPartial: true},
+		input: &pb2.IndirectRequired{
+			StrToNested: map[string]*pb2.NestedWithRequired{
+				"fail": &pb2.NestedWithRequired{},
+			},
+		},
+		want: `{
+  "strToNested": {
+    "fail": {}
+  }
+}`,
+	}, {
+		desc: "indirect required field in oneof",
+		input: &pb2.IndirectRequired{
+			Union: &pb2.IndirectRequired_OneofNested{
+				OneofNested: &pb2.NestedWithRequired{},
+			},
+		},
+		want: `{
+  "oneofNested": {}
+}`,
+		wantErr: true,
+	}, {
+		desc: "indirect required field in oneof with AllowPartial",
+		mo:   jsonpb.MarshalOptions{AllowPartial: true},
+		input: &pb2.IndirectRequired{
+			Union: &pb2.IndirectRequired_OneofNested{
+				OneofNested: &pb2.NestedWithRequired{},
+			},
+		},
+		want: `{
+  "oneofNested": {}
+}`,
+	}, {
 		desc: "unknown fields are ignored",
 		input: &pb2.Scalars{
 			OptString: scalar.String("no unknowns"),