Unmarshal json array into struct array

1.3k views Asked by At

I'm using Mysql 8. I'm also utilizing 99designs/gqlgen to autogenerate the structs based on the GraphQL schema. I was to re-use the same structs when scanning MySql responses. And on top of that, while prototyping, I want to have some JSONs in my table. So the struct is:

type CustomizedItemInput struct {
    Sku              string                 `json:"sku"`
    Name             string                 `json:"name"`
    Skus             []*CustomizedComponent `json:"skus"`
    ...

Since storing(providing Value()) is simpler I managed to store Skus into DB as a top-level JSON successfully. Looks like this:

[{"sku": "123", "position": "LEFT"}, {"sku": "456", "position": "RIGHT"}]

Now, how do I get this value out of DB and back into a array of pointers inside the struct without much hustle? Of course, ideally it should be done without changing the underlying struct because it's autogenerated.

UPDATE: Adding debugging information. I need to read a DB row into CustomizedItemView which basically mirrors CustomizedItemInput from above:

type CustomizedItemView struct {
    Sku              string                     `json:"sku"`
    Name             string                     `json:"name"`
    Skus             []*CustomizedComponentView `json:"skus"`
    ...

Of course, when I say "without hustle" I mean having the DB row extracted into a struct seamlessly. I can add map[string]interface{}{} with all bells and whistles and get the value. But I want to have it neat, like:

    var storedCustItem = model.CustomizedItemView{}
    err := udb.Get(&storedCustItem, database.SelectCustomizationQuery, userID, custItem.Sku, createdAt)

The error I get is:

2020/10/10 20:38:24 sql: Scan error on column index 8, name "skus": unsupported Scan, storing driver.Value type []uint8 into type *[]*model.CustomizedComponentView

(8 because I removed some fields for the example). The main problem is that I can't create Scan() for an unnamed type. I have created wrappers for Value() because my inserts are more verbose and I do type conversion with the wrapper type in them:

type CustomizedComponentsIn []*CustomizedComponent
...
func (customizedComponents CustomizedComponentsIn) Value() (driver.Value, error)
...
tx.MustExec(database.SaveCustomizationCommand,
        custItem.Sku,
        custItem.Name,
        model.CustomizedComponentsIn(custItem.Skus)
...

,which is Ok for inserts because there will be some values that do not belong to the input struct. But I hoped to at least get the value scanned into a View struct automatically.

1

There are 1 answers

0
mkopriva On BEST ANSWER

If you can change the type of the Skus field, the common approach would be to declare a slice type that implements the sql.Scanner and driver.Valuer interfaces and use that instead of the unnamed []*CustomizedComponent type.

For example:

type CustomizedItemInput struct {
    Sku  string                   `json:"sku"`
    Name string                   `json:"name"`
    Skus CustomizedComponentSlice `json:"skus"`
    // ...
}

type CustomizedComponentSlice []*CustomizedComponent

// Value implements driver.Valuer interface.
func (s CustomizedComponentSlice) Value() (driver.Value, error) {
    return json.Marshal(s)
}

// Scan implements sql.Scanner interface.
func (s *CustomizedComponentSlice) Scan(src interface{}) error {
    var data []byte
    switch v := src.(type) {
    case string:
        data = []byte(v)
    case []byte:
        data = v
    default:
        return nil
    }
    return json.Unmarshal(data, s)
}

If you can't change type of the Skus field you will have to explicitly convert the field during scanning.

For example, given the above named slice type, you could do something like this:

v := new(CustomizedItemView)
row := db.QueryRow("SELECT sku, name, skus FROM customized_item_view WHERE sku = ? LIMIT 1", sku)
err := row.Scan(
    &v.Sku,
    &v.Name,
    // do the conversion here, and any other place where you're scanning Skus... 
    (*CustomizedComponentSlice)(&v.Skus),
)
if err != nil {
   return err
}
fmt.Println(v.Skus) // result