Golang: JSON: How do I unmarshal array of strings into []int64

13,100

Solution 1

For anyone interested, I found a solution using a custom type having MarshalJSON() and UnmarshalJSON() methods defined.

type Int64StringSlice []int64

func (slice Int64StringSlice) MarshalJSON() ([]byte, error) {
    values := make([]string, len(slice))
    for i, value := range []int64(slice) {
        values[i] = fmt.Sprintf(`"%v"`, value)
    }

    return []byte(fmt.Sprintf("[%v]", strings.Join(values, ","))), nil
}

func (slice *Int64StringSlice) UnmarshalJSON(b []byte) error {
    // Try array of strings first.
    var values []string
    err := json.Unmarshal(b, &values)
    if err != nil {
        // Fall back to array of integers:
        var values []int64
        if err := json.Unmarshal(b, &values); err != nil {
            return err
        }
        *slice = values
        return nil
    }
    *slice = make([]int64, len(values))
    for i, value := range values {
        value, err := strconv.ParseInt(value, 10, 64)
        if err != nil {
            return err
        }
        (*slice)[i] = value
    }
    return nil
}

The above solution marshals []int64 into JSON string array. Unmarshaling works from both JSON string and integer arrays, ie.:

{"bars": ["1729382256910270462", "309286902808622", "23"]}

{"bars": [1729382256910270462, 309286902808622, 23]}

See example at https://play.golang.org/p/BOqUBGR3DXm

Solution 2

As you quoted from json.Marshal(), the ,string option only applies to specific types, namely:

The "string" option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types.

You want it to work with a slice, but that is not supported by the json package.

If you still want this functionality, you have to write your custom marshaling / unmarshaling logic.

What you presented works, but it is unnecessarily complex. This is because you created your custom logic on slices, but you only want this functionality on individual elements of the slices (arrays). You don't want to change how an array / slice (as a sequence of elements) is rendered or parsed.

So a much simpler solution is to only create a custom "number" type producing this behavior, and elements of slices of this custom type will behave the same.

Our custom number type and the marshaling / unmarshaling logic:

type Int64Str int64

func (i Int64Str) MarshalJSON() ([]byte, error) {
    return json.Marshal(strconv.FormatInt(int64(i), 10))
}

func (i *Int64Str) UnmarshalJSON(b []byte) error {
    // Try string first
    var s string
    if err := json.Unmarshal(b, &s); err == nil {
        value, err := strconv.ParseInt(s, 10, 64)
        if err != nil {
            return err
        }
        *i = Int64Str(value)
        return nil
    }

    // Fallback to number
    return json.Unmarshal(b, (*int64)(i))
}

And that's all!

The type using it:

type Foo struct {
    Bars []Int64Str `json:"bars"`
}

Testing it the same way as you did yields the same result. Try it on the Go Playground.

Share:
13,100

Related videos on Youtube

Vojtech Vitek
Author by

Vojtech Vitek

Blog: vojtechvitek.com Twitter: @VojtechVitek Github: github.com/VojtechVitek

Updated on June 04, 2022

Comments

  • Vojtech Vitek
    Vojtech Vitek almost 2 years

    Golang encoding/json package lets you use ,string struct tag in order to marshal/unmarshal string values (like "309230") into int64 field. Example:

    Int64String int64 `json:",string"`
    

    However, this doesn't work for slices, ie. []int64:

    Int64Slice []int64 `json:",string"` // Doesn't work.
    

    Is there any way to marshal/unmarshal JSON string arrays into []int64 field?


    Quote from https://golang.org/pkg/encoding/json:

    The "string" option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types. This extra level of encoding is sometimes used when communicating with JavaScript programs:

  • Vojtech Vitek
    Vojtech Vitek about 6 years
    Nice, that works too! I didn't think of implementing a custom marshaler on an individual element and then make it a slice :) I guess both implementations are equivalent. The slice approach should be a bit more performant - but only in order of microseconds - which is unlikely a bottle neck for any real app. Thanks for sharing!