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 number
s 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.