Szabolcs
Szabolcs

Reputation: 4086

How to use go's json tag to unmarshal both numeric and string properties into a string value

I have the following Go struct and JSON data:

type Entry struct {
    Timestamp string `json:"timestamp"`
    Value     string `json:"value"`
}
{
  "timestamp": "2020-01-01T00:00:00.000Z",
  "value": "a string" // but sometimes it's a number
}

Most of the time the value of the JSON data is of type string however sometimes it's of type number.

If it's a number the json.Unmarshal method returns an error like this:

json: cannot unmarshal number into Go struct field Entry.valueof type string

Is there an idiomatic and straightforward way to overcome such an issue in Go or I should implement a custom unmarshalling method for this case?

Upvotes: 0

Views: 1302

Answers (2)

icza
icza

Reputation: 417767

You could use interface{} for Entry.Value, and the encoding/json package will choose the proper type at runtime: string for a JSON string, and float64 for a JSON number:

type Entry struct {
    Timestamp string      `json:"timestamp"`
    Value     interface{} `json:"value"`
}

for _, s := range []string{
    `{  "timestamp": "2020-01-01T00:00:00.000Z",  "value": "a string" }`,
    `{  "timestamp": "2020-01-01T00:00:00.000Z",  "value": 12 }`,
} {

    var e Entry
    if err := json.Unmarshal([]byte(s), &e); err != nil {
        panic(err)
    }
    fmt.Printf("%#v %T\n", e, e.Value)
}

This outputs (try it on the Go Playground):

main.Entry{Timestamp:"2020-01-01T00:00:00.000Z", Value:"a string"} string
main.Entry{Timestamp:"2020-01-01T00:00:00.000Z", Value:12} float64

Yes, then you will need a type assertion to get a typed value out from Entry.Value.

Another option is to use json.Number which may accommodate both strings and JSON numbers:

type Entry struct {
    Timestamp string      `json:"timestamp"`
    Value     json.Number `json:"value"`
}

Using this, the above example it outputs (this it on the Go Playground):

main.Entry{Timestamp:"2020-01-01T00:00:00.000Z", Value:"a string"}
main.Entry{Timestamp:"2020-01-01T00:00:00.000Z", Value:"12"}

When using json.Number, you may access its value using Number.String(), or Number.Int64() or Number.Float64() which return an error if its value is not a number.

One thing to note here: if the JSON input is a string that contains a valid number, e.g. "12", then Number.Int64() will not report error but parse it and return 12. This is a difference compared to using intefface{} (where Entry.Value will remain string).

Upvotes: 1

John S Perayil
John S Perayil

Reputation: 6345

Providing an alternative to icza's answer using a custom unmarshaller.

type Entry struct {
    Timestamp string     `json:"timestamp"`
    Value     EntryValue `json:"value"`
}

type EntryValue struct {
    string
}

func (e *EntryValue) UnmarshalJSON(data []byte) error {
    // Simplified check 
    e.string = string(bytes.Trim(data, `"`))
    return nil
}

func main() {

    for _, s := range []string{
        `{  "timestamp": "2020-01-01T00:00:00.000Z",  "value": "a string" }`,
        `{  "timestamp": "2020-01-01T00:00:00.000Z",  "value": 12 }`,
    } {

        var e Entry
        if err := json.Unmarshal([]byte(s), &e); err != nil {
            panic(err)
        }
        fmt.Printf("%#v\n", e)
    }
}

Upvotes: 1

Related Questions