I'm trying to find the best way to map a TypeScript array of multiple values to a struct in Go. What do you recommend?
data: [number, number, number, string, string, boolean, [string, number]?][];
This is the data in JSON format:
{
"data": [
[
35241,
7753,
7750,
"0xbb2b8038a1640196fbe3e38816f3e67cba72d940",
"spot",
true
],
[60259, 7746, null, "#7746/USD", "internal", true, ["requote", 145]]
]
}
For now, what I'm doing is unmarshaling it into a struct using interface{}
Data [][]interface{} `json:"data"`
But then I have to do a type assertion for every field
for _, asset := range assetsAndPairs.Data.Pairs.Data {
fmt.Println(asset[0].(float64))
}
Wondering if there is a better way.
CodePudding user response:
You could define your own struct with a custom unmarshal func like this:
package main
import (
"encoding/json"
"errors"
"fmt"
)
var json_data = []byte(`{
"data": [
[
35241,
7753,
7750,
"0xbb2b8038a1640196fbe3e38816f3e67cba72d940",
"spot",
true
],
[60259, 7746, null, "#7746/USD", "internal", true, ["requote", 145]]
]
}`)
func main() {
var doc Document
if err := json.Unmarshal(json_data, &doc); err != nil {
panic(err)
}
for _, r := range doc.Data {
fmt.Printf("% v\n", r)
if r.Optional != nil {
fmt.Printf(" with optional: % v\n", *r.Optional)
}
}
}
type Document struct {
Data []Row `json:"data"`
}
type Row struct {
SomeInt int
SomeString string
// Simplified
Optional *MoreData
}
type MoreData struct {
SomeString string
SomeInt int
}
func (row *Row) UnmarshalJSON(data []byte) error {
var elements []json.RawMessage
if err := json.Unmarshal(data, &elements); err != nil {
return err
}
if len(elements) < 6 {
return errors.New("too few elements")
}
if err := json.Unmarshal(elements[0], &row.SomeInt); err != nil {
return err
}
if err := json.Unmarshal(elements[3], &row.SomeString); err != nil {
return err
}
if len(elements) == 7 {
var more MoreData
if err := json.Unmarshal(elements[6], &more); err != nil {
return err
}
row.Optional = &more
}
return nil
}
func (more *MoreData) UnmarshalJSON(data []byte) error {
var elements []json.RawMessage
if err := json.Unmarshal(data, &elements); err != nil {
return err
}
if len(elements) < 2 {
return errors.New("too few elements")
}
if err := json.Unmarshal(elements[0], &more.SomeString); err != nil {
return err
}
if err := json.Unmarshal(elements[1], &more.SomeInt); err != nil {
return err
}
return nil
}
https://go.dev/play/p/zLgENBZ0fkC
Based in your example data I'm assuming the numbers are integers. If the JSON can contain floating point numbers (which would be valid number), you would have to adjust the type of course...
CodePudding user response:
I think you could use reflect to define a struct with each field related to an index of data's type and define the UnmarshalJSON function similarly to what @some-user did.
package main
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
)
var json_data = []byte(`{
"data": [
[
35241,
7753,
7750,
"0xbb2b8038a1640196fbe3e38816f3e67cba72d940",
"spot",
true
],
[60259, 7746, null, "#7746/USD", "internal", true, ["requote", 145]]
]
}`)
func main() {
var doc Document
if err := json.Unmarshal(json_data, &doc); err != nil {
panic(err)
}
for _, r := range doc.Data {
fmt.Printf("% v\n", r)
if r.G != nil {
fmt.Printf(" with optional: % v\n", r.G)
}
}
}
type Document struct {
Data []*Row `json:"data"`
}
type Row struct {
A int `idx:"0"`
B int `idx:"1"`
C int `idx:"2"`
D string `idx:"3"`
E string `idx:"4"`
F string `idx:"5"`
G *OptionalItem `idx:"6,optional"`
}
func (r *Row) UnmarshalJSON(data []byte) error {
typ := reflect.TypeOf(r)
val := reflect.ValueOf(r)
return unmarshalArray(typ, val, data)
}
type OptionalItem struct {
A string `idx:"0"`
B string `idx:"1"`
}
func (oi *OptionalItem) UnmarshalJSON(data []byte) error {
typ := reflect.TypeOf(oi)
val := reflect.ValueOf(oi)
return unmarshalArray(typ, val, data)
}
func unmarshalArray(typ reflect.Type, val reflect.Value, data []byte) error {
var items []json.RawMessage
if err := json.Unmarshal(data, &items); err != nil {
return err
}
for i := 0; i < typ.Elem().NumField(); i {
field := typ.Elem().Field(i)
if value, ok := field.Tag.Lookup("idx"); ok {
idx, optional, err := splitTag(value)
if err != nil {
return err
}
if !optional || (optional && len(items) > idx) {
if string(items[idx]) == "null" {
continue
}
valueField := val.Elem().FieldByName(field.Name)
if _, ok := valueField.Interface().(json.Unmarshaler); ok {
if err := json.Unmarshal(items[idx], valueField.Addr().Interface()); err != nil {
return err
}
} else {
if valueField.Kind() == reflect.String {
valueField.SetString(string(items[idx]))
} else if valueField.Kind() == reflect.Int {
number, err := strconv.Atoi(string(items[idx]))
if err != nil {
return err
}
valueField.SetInt(int64(number))
} else {
valueField.Set(reflect.ValueOf(items[idx]))
}
}
}
}
}
return nil
}
func splitTag(tag string) (idx int, optional bool, err error) {
ops := strings.Split(tag, ",")
if len(ops) < 1 {
err = fmt.Errorf("empty 'idx' tag")
return
} else if len(ops) > 1 {
optional = ops[1] == "optional"
}
idx, err = strconv.Atoi(ops[0])
if err != nil {
return
}
return
}
https://go.dev/play/p/x8hC_8JfjGG
There are still some checks missing but it does illustrates the idea.
