Skip to content

Commit 8941d4a

Browse files
authored
Fix JSON unmarshalling in complex structures bug (#22)
* Add documentation in README about JSON marshalling
1 parent 6a4df85 commit 8941d4a

21 files changed

+635
-105
lines changed

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Coverage Status](https://coveralls.io/repos/github/barweiss/go-tuple/badge.svg)](https://coveralls.io/github/barweiss/go-tuple)
55
[![Go Report Card](https://goreportcard.com/badge/github.com/barweiss/go-tuple)](https://goreportcard.com/report/github.com/barweiss/go-tuple)
66
[![Go Reference](https://pkg.go.dev/badge/github.com/barweiss/go-tuple.svg)](https://pkg.go.dev/github.com/barweiss/go-tuple)
7-
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
7+
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
88

99
Go 1.18+ tuple implementation.
1010

@@ -79,6 +79,35 @@ tup := tuple.New2(5, "hi!")
7979
a, b := tup.Values()
8080
```
8181

82+
## JSON Marshalling
83+
84+
Tuples are marshalled and unmarshalled as JSON arrays.
85+
86+
```go
87+
type User struct {
88+
Name string `json:"name"`
89+
Age int `json:"age,omitempty"`
90+
}
91+
92+
type MyJSON struct {
93+
Users []tuple.T2[string, User] `json:"users"`
94+
}
95+
96+
func main() {
97+
data := MyJSON{
98+
Users: []tuple.T2[string, User]{
99+
tuple.New2("foo", User{Name: "foo", Age: 42}),
100+
tuple.New2("bar", User{Name: "bar", Age: 21}),
101+
tuple.New2("baz", User{Name: "baz"}),
102+
},
103+
}
104+
105+
marshalled, _ := json.Marshal(data)
106+
fmt.Printf("%s\n", string(marshalled))
107+
// Outputs: {"users":[["foo",{"name":"foo","age":42}],["bar",{"name":"bar","age":21}],["baz",{"name":"baz"}]]}
108+
}
109+
```
110+
82111
## Comparison
83112

84113
Tuples are compared from the first element to the last.
@@ -101,6 +130,7 @@ fmt.Println(tups) // [["bar", -4, 43], ["foo", 2, -23], ["foo", 72, 15]].
101130
```
102131

103132
---
133+
104134
**NOTE**
105135

106136
In order to compare tuples, all tuple elements must match `constraints.Ordered`.
@@ -165,7 +195,7 @@ func main() {
165195
}
166196
```
167197

168-
In order to call the complex types variation of the comparable functions, __all__ tuple types must match the `Comparable` constraint.
198+
In order to call the complex types variation of the comparable functions, **all** tuple types must match the `Comparable` constraint.
169199

170200
While this is not ideal, this a known inconvenience given the current type parameters capabilities in Go.
171201
Some solutions have been porposed for this issue ([lesser](https://github.com/lelysses/lesser), for example, beatifully articulates the issue),

scripts/gen/tuple.tpl

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,16 +239,21 @@ func (t {{$typeRef}}) MarshalJSON() ([]byte, error) {
239239

240240
// MarshalJSON unmarshals the tuple from a JSON array.
241241
func (t *{{$typeRef}}) UnmarshalJSON(data []byte) error {
242-
var slice []any
242+
// Working with json.RawMessage instead of any enables custom struct support.
243+
var slice []json.RawMessage
243244
if err := json.Unmarshal(data, &slice); err != nil {
244-
return err
245+
return fmt.Errorf("unable to unmarshal json array for tuple: %w", err)
245246
}
246247

247-
unmarshalled, err := FromSlice{{.Len}}[{{.GenericTypesForward}}](slice)
248-
if err != nil {
249-
return err
248+
if len(slice) != {{.Len}} {
249+
return fmt.Errorf("unmarshalled json array length %d must match number of tuple values {{.Len}}", len(slice))
250250
}
251251

252-
*t = unmarshalled
252+
{{- range $index, $num := .Indexes}}
253+
if err := json.Unmarshal(slice[{{$index}}], &t.V{{.}}); err != nil {
254+
return fmt.Errorf("value %q at slice index {{$index}} failed to unmarshal: %w", string(slice[{{$index}}]), err)
255+
}
256+
{{end -}}
257+
253258
return nil
254259
}

scripts/gen/tuple_test.tpl

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -515,15 +515,10 @@ func TestT{{.Len}}_UnmarshalJSON(t *testing.T) {
515515
data: []byte(`["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]`),
516516
wantErr: true,
517517
},
518+
{{range $invalidIndex, $_ := .Indexes}}
518519
{
519-
name: "json array of invalid types",
520-
data: []byte(`[{{range $.Indexes}}{{if ne . 1}},{{end}}{{.}}{{end}}]`),
521-
wantErr: true,
522-
},
523-
{{if gt .Len 1 -}}
524-
{
525-
name: "json array with 1 invalid type",
526-
data: []byte(`[{{range $.Indexes}}{{if ne . 1}},{{end}}{{if eq . 1}}{{.}}{{else}}{{. | quote}}{{end}}{{end}}]`),
520+
name: "json array with invalid type at index {{$invalidIndex}}",
521+
data: []byte(`[{{range $currentIndex, $num := $.Indexes}}{{if ne $currentIndex 0}},{{end}}{{if eq $currentIndex $invalidIndex}}{{$num}}{{else}}{{$num | quote}}{{end}}{{end}}]`),
527522
wantErr: true,
528523
},
529524
{{- end}}
@@ -555,6 +550,25 @@ func TestT{{.Len}}_UnmarshalJSON(t *testing.T) {
555550
}
556551
}
557552

553+
func TestT{{.Len}}_Unmarshal_CustomStruct(t *testing.T) {
554+
type Custom struct {
555+
Name string `json:"name"`
556+
Age int `json:"age"`
557+
}
558+
559+
want := New{{.Len}}({{range .Indexes}}Custom{ Name: {{. | quote}}, Age: {{.}} },{{end}})
560+
var got T{{.Len}}[{{range .Indexes}}Custom,{{end}}]
561+
err := json.Unmarshal([]byte(`[
562+
{{- range .Indexes -}}
563+
{{- if ne . 1}},{{end}}
564+
{ "name": {{. | quote}}, "age": {{.}} }
565+
{{- end}}
566+
]`), &got)
567+
568+
require.NoError(t, err)
569+
require.Equal(t, want, got)
570+
}
571+
558572
func TestT{{.Len}}_Marshal_Unmarshal(t *testing.T) {
559573
tup := New{{.Len}}({{range .Indexes}}{{. | quote}},{{end}})
560574

tuple1.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,16 +204,17 @@ func (t T1[Ty1]) MarshalJSON() ([]byte, error) {
204204

205205
// MarshalJSON unmarshals the tuple from a JSON array.
206206
func (t *T1[Ty1]) UnmarshalJSON(data []byte) error {
207-
var slice []any
207+
// Working with json.RawMessage instead of any enables custom struct support.
208+
var slice []json.RawMessage
208209
if err := json.Unmarshal(data, &slice); err != nil {
209-
return err
210+
return fmt.Errorf("unable to unmarshal json array for tuple: %w", err)
210211
}
211212

212-
unmarshalled, err := FromSlice1[Ty1](slice)
213-
if err != nil {
214-
return err
213+
if len(slice) != 1 {
214+
return fmt.Errorf("unmarshalled json array length %d must match number of tuple values 1", len(slice))
215+
}
216+
if err := json.Unmarshal(slice[0], &t.V1); err != nil {
217+
return fmt.Errorf("value %q at slice index 0 failed to unmarshal: %w", string(slice[0]), err)
215218
}
216-
217-
*t = unmarshalled
218219
return nil
219220
}

tuple1_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,12 +461,12 @@ func TestT1_UnmarshalJSON(t *testing.T) {
461461
data: []byte(`["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]`),
462462
wantErr: true,
463463
},
464+
464465
{
465-
name: "json array of invalid types",
466+
name: "json array with invalid type at index 0",
466467
data: []byte(`[1]`),
467468
wantErr: true,
468469
},
469-
470470
{
471471
name: "json array of valid types",
472472
data: []byte(`["1"]`),
@@ -495,6 +495,22 @@ func TestT1_UnmarshalJSON(t *testing.T) {
495495
}
496496
}
497497

498+
func TestT1_Unmarshal_CustomStruct(t *testing.T) {
499+
type Custom struct {
500+
Name string `json:"name"`
501+
Age int `json:"age"`
502+
}
503+
504+
want := New1(Custom{Name: "1", Age: 1})
505+
var got T1[Custom]
506+
err := json.Unmarshal([]byte(`[
507+
{ "name": "1", "age": 1 }
508+
]`), &got)
509+
510+
require.NoError(t, err)
511+
require.Equal(t, want, got)
512+
}
513+
498514
func TestT1_Marshal_Unmarshal(t *testing.T) {
499515
tup := New1("1")
500516

tuple2.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,21 @@ func (t T2[Ty1, Ty2]) MarshalJSON() ([]byte, error) {
220220

221221
// MarshalJSON unmarshals the tuple from a JSON array.
222222
func (t *T2[Ty1, Ty2]) UnmarshalJSON(data []byte) error {
223-
var slice []any
223+
// Working with json.RawMessage instead of any enables custom struct support.
224+
var slice []json.RawMessage
224225
if err := json.Unmarshal(data, &slice); err != nil {
225-
return err
226+
return fmt.Errorf("unable to unmarshal json array for tuple: %w", err)
226227
}
227228

228-
unmarshalled, err := FromSlice2[Ty1, Ty2](slice)
229-
if err != nil {
230-
return err
229+
if len(slice) != 2 {
230+
return fmt.Errorf("unmarshalled json array length %d must match number of tuple values 2", len(slice))
231+
}
232+
if err := json.Unmarshal(slice[0], &t.V1); err != nil {
233+
return fmt.Errorf("value %q at slice index 0 failed to unmarshal: %w", string(slice[0]), err)
231234
}
232235

233-
*t = unmarshalled
236+
if err := json.Unmarshal(slice[1], &t.V2); err != nil {
237+
return fmt.Errorf("value %q at slice index 1 failed to unmarshal: %w", string(slice[1]), err)
238+
}
234239
return nil
235240
}

tuple2_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,14 +487,15 @@ func TestT2_UnmarshalJSON(t *testing.T) {
487487
data: []byte(`["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]`),
488488
wantErr: true,
489489
},
490+
490491
{
491-
name: "json array of invalid types",
492-
data: []byte(`[1,2]`),
492+
name: "json array with invalid type at index 0",
493+
data: []byte(`[1,"2"]`),
493494
wantErr: true,
494495
},
495496
{
496-
name: "json array with 1 invalid type",
497-
data: []byte(`[1,"2"]`),
497+
name: "json array with invalid type at index 1",
498+
data: []byte(`["1",2]`),
498499
wantErr: true,
499500
},
500501
{
@@ -525,6 +526,23 @@ func TestT2_UnmarshalJSON(t *testing.T) {
525526
}
526527
}
527528

529+
func TestT2_Unmarshal_CustomStruct(t *testing.T) {
530+
type Custom struct {
531+
Name string `json:"name"`
532+
Age int `json:"age"`
533+
}
534+
535+
want := New2(Custom{Name: "1", Age: 1}, Custom{Name: "2", Age: 2})
536+
var got T2[Custom, Custom]
537+
err := json.Unmarshal([]byte(`[
538+
{ "name": "1", "age": 1 },
539+
{ "name": "2", "age": 2 }
540+
]`), &got)
541+
542+
require.NoError(t, err)
543+
require.Equal(t, want, got)
544+
}
545+
528546
func TestT2_Marshal_Unmarshal(t *testing.T) {
529547
tup := New2("1", "2")
530548

tuple3.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,16 +236,25 @@ func (t T3[Ty1, Ty2, Ty3]) MarshalJSON() ([]byte, error) {
236236

237237
// MarshalJSON unmarshals the tuple from a JSON array.
238238
func (t *T3[Ty1, Ty2, Ty3]) UnmarshalJSON(data []byte) error {
239-
var slice []any
239+
// Working with json.RawMessage instead of any enables custom struct support.
240+
var slice []json.RawMessage
240241
if err := json.Unmarshal(data, &slice); err != nil {
241-
return err
242+
return fmt.Errorf("unable to unmarshal json array for tuple: %w", err)
242243
}
243244

244-
unmarshalled, err := FromSlice3[Ty1, Ty2, Ty3](slice)
245-
if err != nil {
246-
return err
245+
if len(slice) != 3 {
246+
return fmt.Errorf("unmarshalled json array length %d must match number of tuple values 3", len(slice))
247+
}
248+
if err := json.Unmarshal(slice[0], &t.V1); err != nil {
249+
return fmt.Errorf("value %q at slice index 0 failed to unmarshal: %w", string(slice[0]), err)
250+
}
251+
252+
if err := json.Unmarshal(slice[1], &t.V2); err != nil {
253+
return fmt.Errorf("value %q at slice index 1 failed to unmarshal: %w", string(slice[1]), err)
247254
}
248255

249-
*t = unmarshalled
256+
if err := json.Unmarshal(slice[2], &t.V3); err != nil {
257+
return fmt.Errorf("value %q at slice index 2 failed to unmarshal: %w", string(slice[2]), err)
258+
}
250259
return nil
251260
}

tuple3_test.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -517,14 +517,20 @@ func TestT3_UnmarshalJSON(t *testing.T) {
517517
data: []byte(`["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]`),
518518
wantErr: true,
519519
},
520+
520521
{
521-
name: "json array of invalid types",
522-
data: []byte(`[1,2,3]`),
522+
name: "json array with invalid type at index 0",
523+
data: []byte(`[1,"2","3"]`),
523524
wantErr: true,
524525
},
525526
{
526-
name: "json array with 1 invalid type",
527-
data: []byte(`[1,"2","3"]`),
527+
name: "json array with invalid type at index 1",
528+
data: []byte(`["1",2,"3"]`),
529+
wantErr: true,
530+
},
531+
{
532+
name: "json array with invalid type at index 2",
533+
data: []byte(`["1","2",3]`),
528534
wantErr: true,
529535
},
530536
{
@@ -555,6 +561,24 @@ func TestT3_UnmarshalJSON(t *testing.T) {
555561
}
556562
}
557563

564+
func TestT3_Unmarshal_CustomStruct(t *testing.T) {
565+
type Custom struct {
566+
Name string `json:"name"`
567+
Age int `json:"age"`
568+
}
569+
570+
want := New3(Custom{Name: "1", Age: 1}, Custom{Name: "2", Age: 2}, Custom{Name: "3", Age: 3})
571+
var got T3[Custom, Custom, Custom]
572+
err := json.Unmarshal([]byte(`[
573+
{ "name": "1", "age": 1 },
574+
{ "name": "2", "age": 2 },
575+
{ "name": "3", "age": 3 }
576+
]`), &got)
577+
578+
require.NoError(t, err)
579+
require.Equal(t, want, got)
580+
}
581+
558582
func TestT3_Marshal_Unmarshal(t *testing.T) {
559583
tup := New3("1", "2", "3")
560584

tuple4.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -252,16 +252,29 @@ func (t T4[Ty1, Ty2, Ty3, Ty4]) MarshalJSON() ([]byte, error) {
252252

253253
// MarshalJSON unmarshals the tuple from a JSON array.
254254
func (t *T4[Ty1, Ty2, Ty3, Ty4]) UnmarshalJSON(data []byte) error {
255-
var slice []any
255+
// Working with json.RawMessage instead of any enables custom struct support.
256+
var slice []json.RawMessage
256257
if err := json.Unmarshal(data, &slice); err != nil {
257-
return err
258+
return fmt.Errorf("unable to unmarshal json array for tuple: %w", err)
258259
}
259260

260-
unmarshalled, err := FromSlice4[Ty1, Ty2, Ty3, Ty4](slice)
261-
if err != nil {
262-
return err
261+
if len(slice) != 4 {
262+
return fmt.Errorf("unmarshalled json array length %d must match number of tuple values 4", len(slice))
263+
}
264+
if err := json.Unmarshal(slice[0], &t.V1); err != nil {
265+
return fmt.Errorf("value %q at slice index 0 failed to unmarshal: %w", string(slice[0]), err)
266+
}
267+
268+
if err := json.Unmarshal(slice[1], &t.V2); err != nil {
269+
return fmt.Errorf("value %q at slice index 1 failed to unmarshal: %w", string(slice[1]), err)
263270
}
264271

265-
*t = unmarshalled
272+
if err := json.Unmarshal(slice[2], &t.V3); err != nil {
273+
return fmt.Errorf("value %q at slice index 2 failed to unmarshal: %w", string(slice[2]), err)
274+
}
275+
276+
if err := json.Unmarshal(slice[3], &t.V4); err != nil {
277+
return fmt.Errorf("value %q at slice index 3 failed to unmarshal: %w", string(slice[3]), err)
278+
}
266279
return nil
267280
}

0 commit comments

Comments
 (0)