Home > other >  How to map a TypeScript array of multiple values to Go struct
How to map a TypeScript array of multiple values to Go struct

Time:09-09

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.

  • Related