Skip to content

Commit 70f441b

Browse files
committed
encoding/json: error when trying to set an embedded pointer to unexported struct types
This CL reverts CL 76851 and takes a different approach to #21357. The changes in encode.go and encode_test.go are reverts that rolls back the changed behavior in CL 76851 where embedded pointers to unexported struct types were unilaterally ignored in both marshal and unmarshal. Instead, these fields are handled as before with the exception that it returns an error when Unmarshal is unable to set an unexported field. The behavior of Marshal is now unchanged with regards to #21357. This policy maintains the greatest degree of backwards compatibility and avoids silently discarding data the user may have expected to be present. Fixes #21357 Change-Id: I7dc753280c99f786ac51acf7e6c0246618c8b2b1 Reviewed-on: https://go-review.googlesource.com/82135 Run-TryBot: Joe Tsai <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 0e76143 commit 70f441b

File tree

4 files changed

+97
-33
lines changed

4 files changed

+97
-33
lines changed

src/encoding/json/decode.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,19 @@ func (d *decodeState) object(v reflect.Value) {
707707
for _, i := range f.index {
708708
if subv.Kind() == reflect.Ptr {
709709
if subv.IsNil() {
710+
// If a struct embeds a pointer to an unexported type,
711+
// it is not possible to set a newly allocated value
712+
// since the field is unexported.
713+
//
714+
// See https://golang.org/issue/21357
715+
if !subv.CanSet() {
716+
d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem()))
717+
// Invalidate subv to ensure d.value(subv) skips over
718+
// the JSON value without assigning it to subv.
719+
subv = reflect.Value{}
720+
destring = false
721+
break
722+
}
710723
subv.Set(reflect.New(subv.Type().Elem()))
711724
}
712725
subv = subv.Elem()

src/encoding/json/decode_test.go

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,6 @@ type embed struct {
195195
Q int
196196
}
197197

198-
type Issue21357 struct {
199-
*embed
200-
R int
201-
}
202-
203198
type Loop struct {
204199
Loop1 int `json:",omitempty"`
205200
Loop2 int `json:",omitempty"`
@@ -871,20 +866,6 @@ var unmarshalTests = []unmarshalTest{
871866
err: fmt.Errorf("json: unknown field \"extra\""),
872867
disallowUnknownFields: true,
873868
},
874-
875-
// Issue 21357.
876-
// Ignore any embedded fields that are pointers to unexported structs.
877-
{
878-
in: `{"Q":1,"R":2}`,
879-
ptr: new(Issue21357),
880-
out: Issue21357{R: 2},
881-
},
882-
{
883-
in: `{"Q":1,"R":2}`,
884-
ptr: new(Issue21357),
885-
err: fmt.Errorf("json: unknown field \"Q\""),
886-
disallowUnknownFields: true,
887-
},
888869
}
889870

890871
func TestMarshal(t *testing.T) {
@@ -2107,3 +2088,81 @@ func TestInvalidStringOption(t *testing.T) {
21072088
t.Fatalf("Unmarshal: %v", err)
21082089
}
21092090
}
2091+
2092+
// Test unmarshal behavior with regards to embedded pointers to unexported structs.
2093+
// If unallocated, this returns an error because unmarshal cannot set the field.
2094+
// Issue 21357.
2095+
func TestUnmarshalEmbeddedPointerUnexported(t *testing.T) {
2096+
type (
2097+
embed1 struct{ Q int }
2098+
embed2 struct{ Q int }
2099+
embed3 struct {
2100+
Q int64 `json:",string"`
2101+
}
2102+
S1 struct {
2103+
*embed1
2104+
R int
2105+
}
2106+
S2 struct {
2107+
*embed1
2108+
Q int
2109+
}
2110+
S3 struct {
2111+
embed1
2112+
R int
2113+
}
2114+
S4 struct {
2115+
*embed1
2116+
embed2
2117+
}
2118+
S5 struct {
2119+
*embed3
2120+
R int
2121+
}
2122+
)
2123+
2124+
tests := []struct {
2125+
in string
2126+
ptr interface{}
2127+
out interface{}
2128+
err error
2129+
}{{
2130+
// Error since we cannot set S1.embed1, but still able to set S1.R.
2131+
in: `{"R":2,"Q":1}`,
2132+
ptr: new(S1),
2133+
out: &S1{R: 2},
2134+
err: fmt.Errorf("json: cannot set embedded pointer to unexported struct: json.embed1"),
2135+
}, {
2136+
// The top level Q field takes precedence.
2137+
in: `{"Q":1}`,
2138+
ptr: new(S2),
2139+
out: &S2{Q: 1},
2140+
}, {
2141+
// No issue with non-pointer variant.
2142+
in: `{"R":2,"Q":1}`,
2143+
ptr: new(S3),
2144+
out: &S3{embed1: embed1{Q: 1}, R: 2},
2145+
}, {
2146+
// No error since both embedded structs have field R, which annihilate each other.
2147+
// Thus, no attempt is made at setting S4.embed1.
2148+
in: `{"R":2}`,
2149+
ptr: new(S4),
2150+
out: new(S4),
2151+
}, {
2152+
// Error since we cannot set S5.embed1, but still able to set S5.R.
2153+
in: `{"R":2,"Q":1}`,
2154+
ptr: new(S5),
2155+
out: &S5{R: 2},
2156+
err: fmt.Errorf("json: cannot set embedded pointer to unexported struct: json.embed3"),
2157+
}}
2158+
2159+
for i, tt := range tests {
2160+
err := Unmarshal([]byte(tt.in), tt.ptr)
2161+
if !reflect.DeepEqual(err, tt.err) {
2162+
t.Errorf("#%d: %v, want %v", i, err, tt.err)
2163+
}
2164+
if !reflect.DeepEqual(tt.ptr, tt.out) {
2165+
t.Errorf("#%d: mismatch\ngot: %#+v\nwant: %#+v", i, tt.ptr, tt.out)
2166+
}
2167+
}
2168+
}

src/encoding/json/encode.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,18 +1094,11 @@ func typeFields(t reflect.Type) []field {
10941094
isUnexported := sf.PkgPath != ""
10951095
if sf.Anonymous {
10961096
t := sf.Type
1097-
isPointer := t.Kind() == reflect.Ptr
1098-
if isPointer {
1097+
if t.Kind() == reflect.Ptr {
10991098
t = t.Elem()
11001099
}
1101-
isStruct := t.Kind() == reflect.Struct
1102-
if isUnexported && (!isStruct || isPointer) {
1103-
// Ignore embedded fields of unexported non-struct types
1104-
// or pointers to unexported struct types.
1105-
//
1106-
// The latter is forbidden because unmarshal is unable
1107-
// to assign a new struct to the unexported field.
1108-
// See https://golang.org/issue/21357
1100+
if isUnexported && t.Kind() != reflect.Struct {
1101+
// Ignore embedded fields of unexported non-struct types.
11091102
continue
11101103
}
11111104
// Do not ignore embedded fields of unexported struct types

src/encoding/json/encode_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,8 @@ func TestAnonymousFields(t *testing.T) {
364364
want: `{"X":2,"Y":4}`,
365365
}, {
366366
// Exported fields of pointers to embedded structs should have their
367-
// exported fields be serialized only for exported struct types.
368-
// Pointers to unexported structs are not allowed since the decoder
369-
// is unable to allocate a struct for that field
367+
// exported fields be serialized regardless of whether the struct types
368+
// themselves are exported.
370369
label: "EmbeddedStructPointer",
371370
makeInput: func() interface{} {
372371
type (
@@ -379,7 +378,7 @@ func TestAnonymousFields(t *testing.T) {
379378
)
380379
return S{&s1{1, 2}, &S2{3, 4}}
381380
},
382-
want: `{"Y":4}`,
381+
want: `{"X":2,"Y":4}`,
383382
}, {
384383
// Exported fields on embedded unexported structs at multiple levels
385384
// of nesting should still be serialized.

0 commit comments

Comments
 (0)