diff --git a/runtime/marshal_json.go b/runtime/marshal_json.go index d6aa8257836..fe52081ab94 100644 --- a/runtime/marshal_json.go +++ b/runtime/marshal_json.go @@ -24,6 +24,11 @@ func (j *JSONBuiltin) Marshal(v interface{}) ([]byte, error) { return json.Marshal(v) } +// MarshalIndent is like Marshal but applies Indent to format the output +func (j *JSONBuiltin) MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { + return json.MarshalIndent(v, prefix, indent) +} + // Unmarshal unmarshals JSON data into "v". func (j *JSONBuiltin) Unmarshal(data []byte, v interface{}) error { return json.Unmarshal(data, v) diff --git a/runtime/marshal_json_test.go b/runtime/marshal_json_test.go index 8fe08aeb9bf..936e10c8ace 100644 --- a/runtime/marshal_json_test.go +++ b/runtime/marshal_json_test.go @@ -38,12 +38,25 @@ func TestJSONBuiltinMarshal(t *testing.T) { } func TestJSONBuiltinMarshalField(t *testing.T) { - var m runtime.JSONBuiltin + var ( + m runtime.JSONBuiltin + buf []byte + err error + ) + for _, fixt := range builtinFieldFixtures { - buf, err := m.Marshal(fixt.data) - if err != nil { - t.Errorf("m.Marshal(%v) failed with %v; want success", fixt.data, err) + if len(fixt.indent) == 0 { + buf, err = m.Marshal(fixt.data) + if err != nil { + t.Errorf("m.Marshal(%v) failed with %v; want success", fixt.data, err) + } + } else { + buf, err = m.MarshalIndent(fixt.data, "", fixt.indent) + if err != nil { + t.Errorf("m.MarshalIndent(%v, \"\", \"%s\") failed with %v; want success", fixt.data, fixt.indent, err) + } } + if got, want := string(buf), fixt.json; got != want { t.Errorf("got = %q; want %q; data = %#v", got, want, fixt.data) } @@ -140,6 +153,18 @@ func TestJSONBuiltinEncoderFields(t *testing.T) { for _, fixt := range builtinFieldFixtures { var buf bytes.Buffer enc := m.NewEncoder(&buf) + + if fixt.indent != "" { + if e, ok := enc.(*json.Encoder); ok { + e.SetIndent("", fixt.indent) + } else { + // By default, JSONBuiltin.NewEncoder returns *json.Encoder as runtime.Encoder. + // Otherwise it's better to fail the tests than skip fixtures with non empty indent + t.Errorf("enc is not *json.Encoder, unable to set indentation settings. " + + "This failure prevents testing the correctness of indentation in JSON output.") + } + } + if err := enc.Encode(fixt.data); err != nil { t.Errorf("enc.Encode(%#v) failed with %v; want success", fixt.data, err) } @@ -189,44 +214,96 @@ func TestJSONBuiltinDecoderFields(t *testing.T) { } var ( + defaultIndent = " " builtinFieldFixtures = []struct { - data interface{} - json string + data interface{} + indent string + json string }{ {data: "", json: `""`}, + {data: "", indent: defaultIndent, json: `""`}, {data: proto.String(""), json: `""`}, + {data: proto.String(""), indent: defaultIndent, json: `""`}, {data: "foo", json: `"foo"`}, + {data: "foo", indent: defaultIndent, json: `"foo"`}, {data: []byte("foo"), json: `"Zm9v"`}, + {data: []byte("foo"), indent: defaultIndent, json: `"Zm9v"`}, {data: []byte{}, json: `""`}, + {data: []byte{}, indent: defaultIndent, json: `""`}, {data: proto.String("foo"), json: `"foo"`}, + {data: proto.String("foo"), indent: defaultIndent, json: `"foo"`}, {data: int32(-1), json: "-1"}, + {data: int32(-1), indent: defaultIndent, json: "-1"}, {data: proto.Int32(-1), json: "-1"}, + {data: proto.Int32(-1), indent: defaultIndent, json: "-1"}, {data: int64(-1), json: "-1"}, + {data: int64(-1), indent: defaultIndent, json: "-1"}, {data: proto.Int64(-1), json: "-1"}, + {data: proto.Int64(-1), indent: defaultIndent, json: "-1"}, {data: uint32(123), json: "123"}, + {data: uint32(123), indent: defaultIndent, json: "123"}, {data: proto.Uint32(123), json: "123"}, + {data: proto.Uint32(123), indent: defaultIndent, json: "123"}, {data: uint64(123), json: "123"}, + {data: uint64(123), indent: defaultIndent, json: "123"}, {data: proto.Uint64(123), json: "123"}, + {data: proto.Uint64(123), indent: defaultIndent, json: "123"}, {data: float32(-1.5), json: "-1.5"}, + {data: float32(-1.5), indent: defaultIndent, json: "-1.5"}, {data: proto.Float32(-1.5), json: "-1.5"}, + {data: proto.Float32(-1.5), indent: defaultIndent, json: "-1.5"}, {data: float64(-1.5), json: "-1.5"}, + {data: float64(-1.5), indent: defaultIndent, json: "-1.5"}, {data: proto.Float64(-1.5), json: "-1.5"}, + {data: proto.Float64(-1.5), indent: defaultIndent, json: "-1.5"}, {data: true, json: "true"}, + {data: true, indent: defaultIndent, json: "true"}, {data: proto.Bool(true), json: "true"}, + {data: proto.Bool(true), indent: defaultIndent, json: "true"}, {data: (*string)(nil), json: "null"}, + {data: (*string)(nil), indent: defaultIndent, json: "null"}, {data: new(emptypb.Empty), json: "{}"}, + {data: new(emptypb.Empty), indent: defaultIndent, json: "{}"}, {data: examplepb.NumericEnum_ONE, json: "1"}, + {data: examplepb.NumericEnum_ONE, indent: defaultIndent, json: "1"}, {data: nil, json: "null"}, + {data: nil, indent: defaultIndent, json: "null"}, {data: (*string)(nil), json: "null"}, + {data: (*string)(nil), indent: defaultIndent, json: "null"}, {data: []interface{}{nil, "foo", -1.0, 1.234, true}, json: `[null,"foo",-1,1.234,true]`}, + {data: []interface{}{nil, "foo", -1.0, 1.234, true}, indent: defaultIndent, json: "[\n null,\n \"foo\",\n -1,\n 1.234,\n true\n]"}, { data: map[string]interface{}{"bar": nil, "baz": -1.0, "fiz": 1.234, "foo": true}, json: `{"bar":null,"baz":-1,"fiz":1.234,"foo":true}`, }, + { + data: map[string]interface{}{"bar": nil, "baz": -1.0, "fiz": 1.234, "foo": true}, + indent: defaultIndent, + json: "{\n \"bar\": null,\n \"baz\": -1,\n \"fiz\": 1.234,\n \"foo\": true\n}", + }, { data: (*examplepb.NumericEnum)(proto.Int32(int32(examplepb.NumericEnum_ONE))), json: "1", }, + { + data: (*examplepb.NumericEnum)(proto.Int32(int32(examplepb.NumericEnum_ONE))), + indent: defaultIndent, + json: "1", + }, + {data: map[string]int{"FOO": 0, "BAR": -1}, json: "{\"BAR\":-1,\"FOO\":0}"}, + {data: map[string]int{"FOO": 0, "BAR": -1}, indent: defaultIndent, json: "{\n \"BAR\": -1,\n \"FOO\": 0\n}"}, + {data: struct { + A string + B int + C map[string]int + }{A: "Go", B: 3, C: map[string]int{"FOO": 0, "BAR": -1}}, + json: "{\"A\":\"Go\",\"B\":3,\"C\":{\"BAR\":-1,\"FOO\":0}}"}, + {data: struct { + A string + B int + C map[string]int + }{A: "Go", B: 3, C: map[string]int{"FOO": 0, "BAR": -1}}, indent: defaultIndent, + json: "{\n \"A\": \"Go\",\n \"B\": 3,\n \"C\": {\n \"BAR\": -1,\n \"FOO\": 0\n }\n}"}, } builtinKnownErrors = []struct { data interface{} diff --git a/runtime/marshal_jsonpb.go b/runtime/marshal_jsonpb.go index 51b8247da2a..8376d1e0efd 100644 --- a/runtime/marshal_jsonpb.go +++ b/runtime/marshal_jsonpb.go @@ -30,10 +30,6 @@ func (*JSONPb) ContentType(_ interface{}) string { // Marshal marshals "v" into JSON. func (j *JSONPb) Marshal(v interface{}) ([]byte, error) { - if _, ok := v.(proto.Message); !ok { - return j.marshalNonProtoField(v) - } - var buf bytes.Buffer if err := j.marshalTo(&buf, v); err != nil { return nil, err @@ -48,9 +44,17 @@ func (j *JSONPb) marshalTo(w io.Writer, v interface{}) error { if err != nil { return err } + if j.Indent != "" { + b := &bytes.Buffer{} + if err := json.Indent(b, buf, "", j.Indent); err != nil { + return err + } + buf = b.Bytes() + } _, err = w.Write(buf) return err } + b, err := j.MarshalOptions.Marshal(p) if err != nil { return err @@ -150,9 +154,6 @@ func (j *JSONPb) marshalNonProtoField(v interface{}) ([]byte, error) { } m[fmt.Sprintf("%v", k.Interface())] = (*json.RawMessage)(&buf) } - if j.Indent != "" { - return json.MarshalIndent(m, "", j.Indent) - } return json.Marshal(m) } if enum, ok := rv.Interface().(protoEnum); ok && !j.UseEnumNumbers { diff --git a/runtime/marshal_jsonpb_test.go b/runtime/marshal_jsonpb_test.go index 43029736e37..806cdb53578 100644 --- a/runtime/marshal_jsonpb_test.go +++ b/runtime/marshal_jsonpb_test.go @@ -132,16 +132,23 @@ func TestJSONPbMarshal(t *testing.T) { func TestJSONPbMarshalFields(t *testing.T) { var m runtime.JSONPb m.UseEnumNumbers = true // builtin fixtures include an enum, expected to be marshaled as int + for _, spec := range builtinFieldFixtures { + m.Indent = spec.indent + buf, err := m.Marshal(spec.data) if err != nil { t.Errorf("m.Marshal(%#v) failed with %v; want success", spec.data, err) } + if got, want := string(buf), spec.json; got != want { t.Errorf("m.Marshal(%#v) = %q; want %q", spec.data, got, want) } } + // Reset m.Indent to ensure no unintended indentation settings carry over to subsequent tests + m.Indent = "" + nums := []examplepb.NumericEnum{examplepb.NumericEnum_ZERO, examplepb.NumericEnum_ONE} buf, err := m.Marshal(nums)