DEV Community

Phani Sajja
Phani Sajja

Posted on

Golang: Marshaling/Unmarshaling String Values and Arrays

In this article, we will delve into the process of marshaling and unmarshaling a JSON property that has the flexibility to hold either a single value or an array of values. For the sake of simplicity, our emphasis will be on handling JSON data that can be marshaled or unmarshaled as a single string or an array of strings.

Background

Let's consider a situation where we map the WHERE clause to its corresponding JSON representation. In the WHERE clause, specific operators accept a single value, while others accept an array of values. To address this scenario, we can utilize the following JSON structure.

{
  "name": "...",
  "operator": "...",
  "value": ....
}
Enter fullscreen mode Exit fullscreen mode

We can address this situation using various methods. One solution is to accept an array of values. Another approach is to allow for either a single value or an array. The first approach does not require any special handling during marshaling or unmarshaling. However, the second approach requires custom marshaling and unmarshaling. This article will further explore and discuss the second approach in detail.

Consider an example where we have the following JSON:

{
  "name": "City",
  "operator": "=",
  "value": "Seattle"
}
Enter fullscreen mode Exit fullscreen mode

Which can be roughly translated into a WHERE condition, as shown below:
... WHERE City = 'Seattle' ...

Consider another example where we have the following JSON:

{
  "name": "City",
  "operator": "IN",
  "value": ["Seattle", "Butte"]
}
Enter fullscreen mode Exit fullscreen mode

Which can be roughly translated into a WHERE condition, as shown below:
... WHERE City IN ('Seattle', 'Butte') ...

As illustrated in the preceding examples, we have the capability to accommodate either a single string value or an array of string values for the "value" property.

Approach

We can model the above JSON in Go using the code snippet below:

type Filter struct {
    Name     string `json:"name"`
    Operator string `json:"operator"`
    Value    Value  `json:"value"`
}

type Value []string
Enter fullscreen mode Exit fullscreen mode

As you can see from the above snippet, the "value" field is of type string slice.

We can unmarshal the "value" from JSON into a variable of type "Value," as demonstrated in the snippet below:

func (v *Value) UnmarshalJSON(data []byte) error {
    if string(data) == "null" || string(data) == `""` {
        return nil
    }

    var iface interface{}
    err := json.Unmarshal(data, &iface)
    if err != nil {
        return err
    }

    switch value := iface.(type) {
    case string:
        *v = Value([]string{value})
        return nil
    case []interface{}:
        vals := make([]string, 0, len(value))
        for _, v := range value {
            val, ok := v.(string)
            if !ok {
                return errors.New("invalid value in array")
            }
            vals = append(vals, val)
        }
        *v = Value(vals)

        return nil
    }

    return errors.New("invalid value type")
}
Enter fullscreen mode Exit fullscreen mode

The marshaling process is demonstrated in the code snippet below:

func (f Filter) MarshalJSON() ([]byte, error) {
    var v interface{}
    if isArrayOperator(f.Operator) {
        v = f.Value
    } else {
        v = strings.Join(f.Value, ",")
    }

    return json.Marshal(map[string]interface{}{
        "name":     f.Name,
        "operator": f.Operator,
        "value":    v,
    })
}

func isArrayOperator(oper string) bool {
    oper = strings.ToUpper(oper)
    if oper == "IN" || oper == "NOT IN" {
        return true
    }

    return false
}
Enter fullscreen mode Exit fullscreen mode

Note: Observe that we have marshaled the Filter struct, not the value type. Here, we can leverage the presence of the operator field to marshal the value accordingly.

The complete code can be found in this gist.

In the above solution, marshaling is determined by the presence of the operator property, may not always hold. To address this, the following code snippet presents an alternative approach for marshaling the value type.

func (v Value) MarshalJSON() ([]byte, error) {
    if len(v) == 0 {
        return json.Marshal("")
    }

    if len(v) == 1 {
        return json.Marshal(v[0])
    }

    var vals []string = []string(v)
    return json.Marshal(vals)
}
Enter fullscreen mode Exit fullscreen mode

In summary, this article provides a technique for custom marshaling and unmarshaling of a value that can either be a string or an array of strings.

Top comments (0)