encoding/jsonpb: basic JSON marshaling

This does not handle extensions, messagesets, and well-known types yet.

Change-Id: I2786c429f490fe8c57f3f85cd25058d936b58bf7
Reviewed-on: https://go-review.googlesource.com/c/162637
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/encoding/jsonpb/encode.go b/encoding/jsonpb/encode.go
new file mode 100644
index 0000000..928b24f
--- /dev/null
+++ b/encoding/jsonpb/encode.go
@@ -0,0 +1,231 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsonpb
+
+import (
+	"encoding/base64"
+	"math"
+	"sort"
+
+	"github.com/golang/protobuf/v2/internal/encoding/json"
+	"github.com/golang/protobuf/v2/internal/errors"
+	"github.com/golang/protobuf/v2/internal/pragma"
+	"github.com/golang/protobuf/v2/proto"
+	pref "github.com/golang/protobuf/v2/reflect/protoreflect"
+)
+
+// Marshal writes the given proto.Message in JSON format using default options.
+func Marshal(m proto.Message) ([]byte, error) {
+	return MarshalOptions{}.Marshal(m)
+}
+
+// MarshalOptions is a configurable JSON format marshaler.
+type MarshalOptions struct {
+	pragma.NoUnkeyedLiterals
+
+	// Set Compact to true to have output in a single line with no line breaks.
+	Compact bool
+}
+
+// Marshal writes the given proto.Message in JSON format using options in MarshalOptions object.
+func (o MarshalOptions) Marshal(m proto.Message) ([]byte, error) {
+	var nerr errors.NonFatal
+	v, err := o.marshalMessage(m.ProtoReflect())
+	if !nerr.Merge(err) {
+		return nil, err
+	}
+
+	indent := "  "
+	if o.Compact {
+		indent = ""
+	}
+
+	b, err := json.Marshal(v, indent)
+	if !nerr.Merge(err) {
+		return nil, err
+	}
+	return b, nerr.E
+}
+
+// marshalMessage converts a protoreflect.Message to a json.Value.
+func (o MarshalOptions) marshalMessage(m pref.Message) (json.Value, error) {
+	var nerr errors.NonFatal
+	var msgFields [][2]json.Value
+
+	msgType := m.Type()
+	fieldDescs := msgType.Fields()
+	knownFields := m.KnownFields()
+	size := fieldDescs.Len()
+	for i := 0; i < size; i++ {
+		fd := fieldDescs.Get(i)
+		num := fd.Number()
+
+		if !knownFields.Has(num) {
+			if fd.Cardinality() == pref.Required {
+				// Treat unset required fields as a non-fatal error.
+				nerr.AppendRequiredNotSet(string(fd.FullName()))
+			}
+			continue
+		}
+
+		name := json.ValueOf(fd.JSONName())
+		pval := knownFields.Get(num)
+		var err error
+		msgFields, err = o.appendField(msgFields, name, pval, fd)
+		if !nerr.Merge(err) {
+			return json.Value{}, err
+		}
+	}
+
+	return json.ValueOf(msgFields), nerr.E
+}
+
+// appendField marshals a protoreflect.Value and appends it to the given
+// [][2]json.Value.
+func (o MarshalOptions) appendField(msgFields [][2]json.Value, name json.Value, pval pref.Value, fd pref.FieldDescriptor) ([][2]json.Value, error) {
+	var nerr errors.NonFatal
+	var jval json.Value
+	var err error
+
+	if fd.Cardinality() == pref.Repeated {
+		// Map or repeated fields.
+		if fd.IsMap() {
+			jval, err = o.marshalMap(pval.Map(), fd)
+			if !nerr.Merge(err) {
+				return msgFields, err
+			}
+		} else {
+			jval, err = o.marshalList(pval.List(), fd)
+			if !nerr.Merge(err) {
+				return msgFields, err
+			}
+		}
+	} else {
+		// Required or optional fields.
+		jval, err = o.marshalSingular(pval, fd)
+		if !nerr.Merge(err) {
+			return msgFields, err
+		}
+	}
+
+	msgFields = append(msgFields, [2]json.Value{name, jval})
+	return msgFields, nerr.E
+}
+
+// marshalSingular converts a non-repeated field value to json.Value.
+// This includes all scalar types, enums, messages, and groups.
+func (o MarshalOptions) marshalSingular(val pref.Value, fd pref.FieldDescriptor) (json.Value, error) {
+	kind := fd.Kind()
+	switch kind {
+	case pref.BoolKind, pref.StringKind,
+		pref.Int32Kind, pref.Sint32Kind, pref.Uint32Kind,
+		pref.Sfixed32Kind, pref.Fixed32Kind:
+		return json.ValueOf(val.Interface()), nil
+
+	case pref.Int64Kind, pref.Sint64Kind, pref.Uint64Kind,
+		pref.Sfixed64Kind, pref.Fixed64Kind:
+		return json.ValueOf(val.String()), nil
+
+	case pref.FloatKind, pref.DoubleKind:
+		n := val.Float()
+		switch {
+		case math.IsNaN(n):
+			return json.ValueOf("NaN"), nil
+		case math.IsInf(n, +1):
+			return json.ValueOf("Infinity"), nil
+		case math.IsInf(n, -1):
+			return json.ValueOf("-Infinity"), nil
+		default:
+			return json.ValueOf(n), nil
+		}
+
+	case pref.BytesKind:
+		return json.ValueOf(base64.StdEncoding.EncodeToString(val.Bytes())), nil
+
+	case pref.EnumKind:
+		num := val.Enum()
+		if desc := fd.EnumType().Values().ByNumber(num); desc != nil {
+			return json.ValueOf(string(desc.Name())), nil
+		}
+		// Use numeric value if there is no enum value descriptor.
+		return json.ValueOf(int32(num)), nil
+
+	case pref.MessageKind, pref.GroupKind:
+		return o.marshalMessage(val.Message())
+	}
+
+	return json.Value{}, errors.New("%v has unknown kind: %v", fd.FullName(), kind)
+}
+
+// marshalList converts a protoreflect.List to json.Value.
+func (o MarshalOptions) marshalList(list pref.List, fd pref.FieldDescriptor) (json.Value, error) {
+	var nerr errors.NonFatal
+	size := list.Len()
+	values := make([]json.Value, 0, size)
+
+	for i := 0; i < size; i++ {
+		item := list.Get(i)
+		val, err := o.marshalSingular(item, fd)
+		if !nerr.Merge(err) {
+			return json.Value{}, err
+		}
+		values = append(values, val)
+	}
+
+	return json.ValueOf(values), nerr.E
+}
+
+type mapEntry struct {
+	key   pref.MapKey
+	value pref.Value
+}
+
+// marshalMap converts a protoreflect.Map to json.Value.
+func (o MarshalOptions) marshalMap(mmap pref.Map, fd pref.FieldDescriptor) (json.Value, error) {
+	msgFields := fd.MessageType().Fields()
+	keyType := msgFields.ByNumber(1)
+	valType := msgFields.ByNumber(2)
+
+	// Get a sorted list based on keyType first.
+	entries := make([]mapEntry, 0, mmap.Len())
+	mmap.Range(func(key pref.MapKey, val pref.Value) bool {
+		entries = append(entries, mapEntry{key: key, value: val})
+		return true
+	})
+	sortMap(keyType.Kind(), entries)
+
+	// Convert to list of [2]json.Value.
+	var nerr errors.NonFatal
+	values := make([][2]json.Value, 0, len(entries))
+	for _, entry := range entries {
+		jkey := json.ValueOf(entry.key.String())
+		jval, err := o.marshalSingular(entry.value, valType)
+		if !nerr.Merge(err) {
+			return json.Value{}, err
+		}
+		values = append(values, [2]json.Value{jkey, jval})
+	}
+	return json.ValueOf(values), nerr.E
+}
+
+// sortMap orders list based on value of key field for deterministic output.
+func sortMap(keyKind pref.Kind, values []mapEntry) {
+	less := func(i, j int) bool {
+		return values[i].key.String() < values[j].key.String()
+	}
+	switch keyKind {
+	case pref.Int32Kind, pref.Sint32Kind, pref.Sfixed32Kind,
+		pref.Int64Kind, pref.Sint64Kind, pref.Sfixed64Kind:
+		less = func(i, j int) bool {
+			return values[i].key.Int() < values[j].key.Int()
+		}
+	case pref.Uint32Kind, pref.Fixed32Kind,
+		pref.Uint64Kind, pref.Fixed64Kind:
+		less = func(i, j int) bool {
+			return values[i].key.Uint() < values[j].key.Uint()
+		}
+	}
+	sort.Slice(values, less)
+}
diff --git a/encoding/jsonpb/encode_test.go b/encoding/jsonpb/encode_test.go
new file mode 100644
index 0000000..5ee09b2
--- /dev/null
+++ b/encoding/jsonpb/encode_test.go
@@ -0,0 +1,727 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsonpb_test
+
+import (
+	"math"
+	"strings"
+	"testing"
+
+	"github.com/golang/protobuf/v2/encoding/jsonpb"
+	"github.com/golang/protobuf/v2/internal/encoding/pack"
+	"github.com/golang/protobuf/v2/internal/scalar"
+	"github.com/golang/protobuf/v2/proto"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+
+	// The legacy package must be imported prior to use of any legacy messages.
+	// TODO: Remove this when protoV1 registers these hooks for you.
+	_ "github.com/golang/protobuf/v2/internal/legacy"
+
+	"github.com/golang/protobuf/v2/encoding/testprotos/pb2"
+	"github.com/golang/protobuf/v2/encoding/testprotos/pb3"
+)
+
+// splitLines is a cmpopts.Option for comparing strings with line breaks.
+var splitLines = cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {
+	return strings.Split(s, "\n")
+})
+
+func pb2Enum(i int32) *pb2.Enum {
+	p := new(pb2.Enum)
+	*p = pb2.Enum(i)
+	return p
+}
+
+func pb2Enums_NestedEnum(i int32) *pb2.Enums_NestedEnum {
+	p := new(pb2.Enums_NestedEnum)
+	*p = pb2.Enums_NestedEnum(i)
+	return p
+}
+
+func TestMarshal(t *testing.T) {
+	tests := []struct {
+		desc  string
+		mo    jsonpb.MarshalOptions
+		input proto.Message
+		want  string
+	}{{
+		desc:  "proto2 optional scalars not set",
+		input: &pb2.Scalars{},
+		want:  "{}",
+	}, {
+		desc:  "proto3 scalars not set",
+		input: &pb3.Scalars{},
+		want:  "{}",
+	}, {
+		desc: "proto2 optional scalars set to zero values",
+		input: &pb2.Scalars{
+			OptBool:     scalar.Bool(false),
+			OptInt32:    scalar.Int32(0),
+			OptInt64:    scalar.Int64(0),
+			OptUint32:   scalar.Uint32(0),
+			OptUint64:   scalar.Uint64(0),
+			OptSint32:   scalar.Int32(0),
+			OptSint64:   scalar.Int64(0),
+			OptFixed32:  scalar.Uint32(0),
+			OptFixed64:  scalar.Uint64(0),
+			OptSfixed32: scalar.Int32(0),
+			OptSfixed64: scalar.Int64(0),
+			OptFloat:    scalar.Float32(0),
+			OptDouble:   scalar.Float64(0),
+			OptBytes:    []byte{},
+			OptString:   scalar.String(""),
+		},
+		want: `{
+  "optBool": false,
+  "optInt32": 0,
+  "optInt64": "0",
+  "optUint32": 0,
+  "optUint64": "0",
+  "optSint32": 0,
+  "optSint64": "0",
+  "optFixed32": 0,
+  "optFixed64": "0",
+  "optSfixed32": 0,
+  "optSfixed64": "0",
+  "optFloat": 0,
+  "optDouble": 0,
+  "optBytes": "",
+  "optString": ""
+}`,
+	}, {
+		desc: "proto2 optional scalars set to some values",
+		input: &pb2.Scalars{
+			OptBool:     scalar.Bool(true),
+			OptInt32:    scalar.Int32(0xff),
+			OptInt64:    scalar.Int64(0xdeadbeef),
+			OptUint32:   scalar.Uint32(47),
+			OptUint64:   scalar.Uint64(0xdeadbeef),
+			OptSint32:   scalar.Int32(-1001),
+			OptSint64:   scalar.Int64(-0xffff),
+			OptFixed64:  scalar.Uint64(64),
+			OptSfixed32: scalar.Int32(-32),
+			OptFloat:    scalar.Float32(1.02),
+			OptDouble:   scalar.Float64(1.234),
+			OptBytes:    []byte("\xe8\xb0\xb7\xe6\xad\x8c"),
+			OptString:   scalar.String("谷歌"),
+		},
+		want: `{
+  "optBool": true,
+  "optInt32": 255,
+  "optInt64": "3735928559",
+  "optUint32": 47,
+  "optUint64": "3735928559",
+  "optSint32": -1001,
+  "optSint64": "-65535",
+  "optFixed64": "64",
+  "optSfixed32": -32,
+  "optFloat": 1.02,
+  "optDouble": 1.234,
+  "optBytes": "6LC35q2M",
+  "optString": "谷歌"
+}`,
+	}, {
+		desc: "float nan",
+		input: &pb3.Scalars{
+			SFloat: float32(math.NaN()),
+		},
+		want: `{
+  "sFloat": "NaN"
+}`,
+	}, {
+		desc: "float positive infinity",
+		input: &pb3.Scalars{
+			SFloat: float32(math.Inf(1)),
+		},
+		want: `{
+  "sFloat": "Infinity"
+}`,
+	}, {
+		desc: "float negative infinity",
+		input: &pb3.Scalars{
+			SFloat: float32(math.Inf(-1)),
+		},
+		want: `{
+  "sFloat": "-Infinity"
+}`,
+	}, {
+		desc: "double nan",
+		input: &pb3.Scalars{
+			SDouble: math.NaN(),
+		},
+		want: `{
+  "sDouble": "NaN"
+}`,
+	}, {
+		desc: "double positive infinity",
+		input: &pb3.Scalars{
+			SDouble: math.Inf(1),
+		},
+		want: `{
+  "sDouble": "Infinity"
+}`,
+	}, {
+		desc: "double negative infinity",
+		input: &pb3.Scalars{
+			SDouble: math.Inf(-1),
+		},
+		want: `{
+  "sDouble": "-Infinity"
+}`,
+	}, {
+		desc:  "proto2 enum not set",
+		input: &pb2.Enums{},
+		want:  "{}",
+	}, {
+		desc: "proto2 enum set to zero value",
+		input: &pb2.Enums{
+			OptEnum:       pb2Enum(0),
+			OptNestedEnum: pb2Enums_NestedEnum(0),
+		},
+		want: `{
+  "optEnum": 0,
+  "optNestedEnum": 0
+}`,
+	}, {
+		desc: "proto2 enum",
+		input: &pb2.Enums{
+			OptEnum:       pb2.Enum_ONE.Enum(),
+			OptNestedEnum: pb2.Enums_UNO.Enum(),
+		},
+		want: `{
+  "optEnum": "ONE",
+  "optNestedEnum": "UNO"
+}`,
+	}, {
+		desc: "proto2 enum set to numeric values",
+		input: &pb2.Enums{
+			OptEnum:       pb2Enum(2),
+			OptNestedEnum: pb2Enums_NestedEnum(2),
+		},
+		want: `{
+  "optEnum": "TWO",
+  "optNestedEnum": "DOS"
+}`,
+	}, {
+		desc: "proto2 enum set to unnamed numeric values",
+		input: &pb2.Enums{
+			OptEnum:       pb2Enum(101),
+			OptNestedEnum: pb2Enums_NestedEnum(-101),
+		},
+		want: `{
+  "optEnum": 101,
+  "optNestedEnum": -101
+}`,
+	}, {
+		desc:  "proto3 enum not set",
+		input: &pb3.Enums{},
+		want:  "{}",
+	}, {
+		desc: "proto3 enum set to zero value",
+		input: &pb3.Enums{
+			SEnum:       pb3.Enum_ZERO,
+			SNestedEnum: pb3.Enums_CERO,
+		},
+		want: "{}",
+	}, {
+		desc: "proto3 enum",
+		input: &pb3.Enums{
+			SEnum:       pb3.Enum_ONE,
+			SNestedEnum: pb3.Enums_UNO,
+		},
+		want: `{
+  "sEnum": "ONE",
+  "sNestedEnum": "UNO"
+}`,
+	}, {
+		desc: "proto3 enum set to numeric values",
+		input: &pb3.Enums{
+			SEnum:       2,
+			SNestedEnum: 2,
+		},
+		want: `{
+  "sEnum": "TWO",
+  "sNestedEnum": "DOS"
+}`,
+	}, {
+		desc: "proto3 enum set to unnamed numeric values",
+		input: &pb3.Enums{
+			SEnum:       -47,
+			SNestedEnum: 47,
+		},
+		want: `{
+  "sEnum": -47,
+  "sNestedEnum": 47
+}`,
+	}, {
+		desc:  "proto2 nested message not set",
+		input: &pb2.Nests{},
+		want:  "{}",
+	}, {
+		desc: "proto2 nested message set to empty",
+		input: &pb2.Nests{
+			OptNested: &pb2.Nested{},
+			Optgroup:  &pb2.Nests_OptGroup{},
+		},
+		want: `{
+  "optNested": {},
+  "optgroup": {}
+}`,
+	}, {
+		desc: "proto2 nested messages",
+		input: &pb2.Nests{
+			OptNested: &pb2.Nested{
+				OptString: scalar.String("nested message"),
+				OptNested: &pb2.Nested{
+					OptString: scalar.String("another nested message"),
+				},
+			},
+		},
+		want: `{
+  "optNested": {
+    "optString": "nested message",
+    "optNested": {
+      "optString": "another nested message"
+    }
+  }
+}`,
+	}, {
+		desc: "proto2 groups",
+		input: &pb2.Nests{
+			Optgroup: &pb2.Nests_OptGroup{
+				OptString: scalar.String("inside a group"),
+				OptNested: &pb2.Nested{
+					OptString: scalar.String("nested message inside a group"),
+				},
+				Optnestedgroup: &pb2.Nests_OptGroup_OptNestedGroup{
+					OptFixed32: scalar.Uint32(47),
+				},
+			},
+		},
+		want: `{
+  "optgroup": {
+    "optString": "inside a group",
+    "optNested": {
+      "optString": "nested message inside a group"
+    },
+    "optnestedgroup": {
+      "optFixed32": 47
+    }
+  }
+}`,
+	}, {
+		desc:  "proto3 nested message not set",
+		input: &pb3.Nests{},
+		want:  "{}",
+	}, {
+		desc: "proto3 nested message set to empty",
+		input: &pb3.Nests{
+			SNested: &pb3.Nested{},
+		},
+		want: `{
+  "sNested": {}
+}`,
+	}, {
+		desc: "proto3 nested message",
+		input: &pb3.Nests{
+			SNested: &pb3.Nested{
+				SString: "nested message",
+				SNested: &pb3.Nested{
+					SString: "another nested message",
+				},
+			},
+		},
+		want: `{
+  "sNested": {
+    "sString": "nested message",
+    "sNested": {
+      "sString": "another nested message"
+    }
+  }
+}`,
+	}, {
+		desc:  "oneof not set",
+		input: &pb3.Oneofs{},
+		want:  "{}",
+	}, {
+		desc: "oneof set to empty string",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofString{},
+		},
+		want: `{
+  "oneofString": ""
+}`,
+	}, {
+		desc: "oneof set to string",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofString{
+				OneofString: "hello",
+			},
+		},
+		want: `{
+  "oneofString": "hello"
+}`,
+	}, {
+		desc: "oneof set to enum",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofEnum{
+				OneofEnum: pb3.Enum_ZERO,
+			},
+		},
+		want: `{
+  "oneofEnum": "ZERO"
+}`,
+	}, {
+		desc: "oneof set to empty message",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofNested{
+				OneofNested: &pb3.Nested{},
+			},
+		},
+		want: `{
+  "oneofNested": {}
+}`,
+	}, {
+		desc: "oneof set to message",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofNested{
+				OneofNested: &pb3.Nested{
+					SString: "nested message",
+				},
+			},
+		},
+		want: `{
+  "oneofNested": {
+    "sString": "nested message"
+  }
+}`,
+	}, {
+		desc:  "repeated fields not set",
+		input: &pb2.Repeats{},
+		want:  "{}",
+	}, {
+		desc: "repeated fields set to empty slices",
+		input: &pb2.Repeats{
+			RptBool:   []bool{},
+			RptInt32:  []int32{},
+			RptInt64:  []int64{},
+			RptUint32: []uint32{},
+			RptUint64: []uint64{},
+			RptFloat:  []float32{},
+			RptDouble: []float64{},
+			RptBytes:  [][]byte{},
+		},
+		want: "{}",
+	}, {
+		desc: "repeated fields set to some values",
+		input: &pb2.Repeats{
+			RptBool:   []bool{true, false, true, true},
+			RptInt32:  []int32{1, 6, 0, 0},
+			RptInt64:  []int64{-64, 47},
+			RptUint32: []uint32{0xff, 0xffff},
+			RptUint64: []uint64{0xdeadbeef},
+			RptFloat:  []float32{float32(math.NaN()), float32(math.Inf(1)), float32(math.Inf(-1)), 1.034},
+			RptDouble: []float64{math.NaN(), math.Inf(1), math.Inf(-1), 1.23e-308},
+			RptString: []string{"hello", "世界"},
+			RptBytes: [][]byte{
+				[]byte("hello"),
+				[]byte("\xe4\xb8\x96\xe7\x95\x8c"),
+			},
+		},
+		want: `{
+  "rptBool": [
+    true,
+    false,
+    true,
+    true
+  ],
+  "rptInt32": [
+    1,
+    6,
+    0,
+    0
+  ],
+  "rptInt64": [
+    "-64",
+    "47"
+  ],
+  "rptUint32": [
+    255,
+    65535
+  ],
+  "rptUint64": [
+    "3735928559"
+  ],
+  "rptFloat": [
+    "NaN",
+    "Infinity",
+    "-Infinity",
+    1.034
+  ],
+  "rptDouble": [
+    "NaN",
+    "Infinity",
+    "-Infinity",
+    1.23e-308
+  ],
+  "rptString": [
+    "hello",
+    "世界"
+  ],
+  "rptBytes": [
+    "aGVsbG8=",
+    "5LiW55WM"
+  ]
+}`,
+	}, {
+		desc: "repeated enums",
+		input: &pb2.Enums{
+			RptEnum:       []pb2.Enum{pb2.Enum_ONE, 2, pb2.Enum_TEN, 42},
+			RptNestedEnum: []pb2.Enums_NestedEnum{2, 47, 10},
+		},
+		want: `{
+  "rptEnum": [
+    "ONE",
+    "TWO",
+    "TEN",
+    42
+  ],
+  "rptNestedEnum": [
+    "DOS",
+    47,
+    "DIEZ"
+  ]
+}`,
+	}, {
+		desc: "repeated messages set to empty",
+		input: &pb2.Nests{
+			RptNested: []*pb2.Nested{},
+			Rptgroup:  []*pb2.Nests_RptGroup{},
+		},
+		want: "{}",
+	}, {
+		desc: "repeated messages",
+		input: &pb2.Nests{
+			RptNested: []*pb2.Nested{
+				{
+					OptString: scalar.String("repeat nested one"),
+				},
+				{
+					OptString: scalar.String("repeat nested two"),
+					OptNested: &pb2.Nested{
+						OptString: scalar.String("inside repeat nested two"),
+					},
+				},
+				{},
+			},
+		},
+		want: `{
+  "rptNested": [
+    {
+      "optString": "repeat nested one"
+    },
+    {
+      "optString": "repeat nested two",
+      "optNested": {
+        "optString": "inside repeat nested two"
+      }
+    },
+    {}
+  ]
+}`,
+	}, {
+		desc: "repeated messages contains nil value",
+		input: &pb2.Nests{
+			RptNested: []*pb2.Nested{nil, {}},
+		},
+		want: `{
+  "rptNested": [
+    {},
+    {}
+  ]
+}`,
+	}, {
+		desc: "repeated groups",
+		input: &pb2.Nests{
+			Rptgroup: []*pb2.Nests_RptGroup{
+				{
+					RptString: []string{"hello", "world"},
+				},
+				{},
+				nil,
+			},
+		},
+		want: `{
+  "rptgroup": [
+    {
+      "rptString": [
+        "hello",
+        "world"
+      ]
+    },
+    {},
+    {}
+  ]
+}`,
+	}, {
+		desc:  "map fields not set",
+		input: &pb3.Maps{},
+		want:  "{}",
+	}, {
+		desc: "map fields set to empty",
+		input: &pb3.Maps{
+			Int32ToStr:   map[int32]string{},
+			BoolToUint32: map[bool]uint32{},
+			Uint64ToEnum: map[uint64]pb3.Enum{},
+			StrToNested:  map[string]*pb3.Nested{},
+			StrToOneofs:  map[string]*pb3.Oneofs{},
+		},
+		want: "{}",
+	}, {
+		desc: "map fields 1",
+		input: &pb3.Maps{
+			BoolToUint32: map[bool]uint32{
+				true:  42,
+				false: 101,
+			},
+		},
+		want: `{
+  "boolToUint32": {
+    "false": 101,
+    "true": 42
+  }
+}`,
+	}, {
+		desc: "map fields 2",
+		input: &pb3.Maps{
+			Int32ToStr: map[int32]string{
+				-101: "-101",
+				0xff: "0xff",
+				0:    "zero",
+			},
+		},
+		want: `{
+  "int32ToStr": {
+    "-101": "-101",
+    "0": "zero",
+    "255": "0xff"
+  }
+}`,
+	}, {
+		desc: "map fields 3",
+		input: &pb3.Maps{
+			Uint64ToEnum: map[uint64]pb3.Enum{
+				1:  pb3.Enum_ONE,
+				2:  pb3.Enum_TWO,
+				10: pb3.Enum_TEN,
+				47: 47,
+			},
+		},
+		want: `{
+  "uint64ToEnum": {
+    "1": "ONE",
+    "2": "TWO",
+    "10": "TEN",
+    "47": 47
+  }
+}`,
+	}, {
+		desc: "map fields 4",
+		input: &pb3.Maps{
+			StrToNested: map[string]*pb3.Nested{
+				"nested": &pb3.Nested{
+					SString: "nested in a map",
+				},
+			},
+		},
+		want: `{
+  "strToNested": {
+    "nested": {
+      "sString": "nested in a map"
+    }
+  }
+}`,
+	}, {
+		desc: "map fields 5",
+		input: &pb3.Maps{
+			StrToOneofs: map[string]*pb3.Oneofs{
+				"string": &pb3.Oneofs{
+					Union: &pb3.Oneofs_OneofString{
+						OneofString: "hello",
+					},
+				},
+				"nested": &pb3.Oneofs{
+					Union: &pb3.Oneofs_OneofNested{
+						OneofNested: &pb3.Nested{
+							SString: "nested oneof in map field value",
+						},
+					},
+				},
+			},
+		},
+		want: `{
+  "strToOneofs": {
+    "nested": {
+      "oneofNested": {
+        "sString": "nested oneof in map field value"
+      }
+    },
+    "string": {
+      "oneofString": "hello"
+    }
+  }
+}`,
+	}, {
+		desc: "map field contains nil value",
+		input: &pb3.Maps{
+			StrToNested: map[string]*pb3.Nested{
+				"nil": nil,
+			},
+		},
+		want: `{
+  "strToNested": {
+    "nil": {}
+  }
+}`,
+	}, {
+		desc: "unknown fields are ignored",
+		input: &pb2.Scalars{
+			OptString: scalar.String("no unknowns"),
+			XXX_unrecognized: pack.Message{
+				pack.Tag{101, pack.BytesType}, pack.String("hello world"),
+			}.Marshal(),
+		},
+		want: `{
+  "optString": "no unknowns"
+}`,
+	}, {
+		desc: "json_name",
+		input: &pb3.JSONNames{
+			SString: "json_name",
+		},
+		want: `{
+  "foo_bar": "json_name"
+}`,
+	}}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			t.Parallel()
+			b, err := tt.mo.Marshal(tt.input)
+			if err != nil {
+				t.Errorf("Marshal() returned error: %v\n", err)
+			}
+			got := string(b)
+			if got != tt.want {
+				t.Errorf("Marshal()\n<got>\n%v\n<want>\n%v\n", got, tt.want)
+				if diff := cmp.Diff(tt.want, got, splitLines); diff != "" {
+					t.Errorf("Marshal() diff -want +got\n%v\n", diff)
+				}
+			}
+		})
+	}
+}