DEV Community

leofigy
leofigy

Posted on

Inconsistent json unmarshal

Common daily task

Go provides has a json package in the standard library. However a common case we face everyday at work is that our input is not consistent, that means a json entry could have different data types.

Consider the following input jsons

# good input
{
    "name": "Sam smith",
    "age" : 34
}
# lazy input 
{
    "name": "Sam smith stringer",
    "age" : "41"
}

as one can see the "age" field has two different types string and number.
Go provides a trick to handle this case; So the recipe is

  • Write your go struct with the right types
  • Create an Alias for your struct to avoid recursively unmarshal
  • Unmarshal to your intermediate struct with your embbeded alias
  • Select the attributes of your struct and parse them to the intended type

Full code snippet

package main

import (
    "fmt"
    "encoding/json"
    "strconv"
)

type InputJSON struct {
    Name string `json:"name,omitempty"`
    Age  int    `json:"age,omitempty"`
}

/* Alias to avoid calling implicit UnmarshalJSON of your original 
   struct 
*/
type Alias InputJSON

/* write your Unmarshal function using an intermediate struct to use interface to handle any type */
func (i *InputJSON) UnmarshalJSON(input []byte) error {
    // intermediate step
    middle := struct {
        Alias 
        Age interface{} `json:"age,omitempty"`
    }{}

    if err := json.Unmarshal(input, &middle); err != nil {
        return err
    }

    *i = InputJSON(middle.Alias)

    if middle.Age != nil {
        switch realValue := (middle.Age).(type) {
        case string:
            intValue, err := strconv.Atoi(realValue)
            if err != nil {
                return err
            }
            i.Age = intValue
        case float64:
            // by default uses a float64 for number representation
            i.Age = int(realValue)
        }
    }
    return nil
}

func main() {
    // use cases 
    expectedCase := []byte(`
        {
            "name": "Sam smith",
            "age" : 34
        }
    `)

    awfulCase := []byte(`
        {
            "name": "Sam smith stringer",
            "age" : "41"
        }
    `)

    goodInput := InputJSON{}

    if err := json.Unmarshal(expectedCase, &goodInput); err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", goodInput)

    badInput := InputJSON{}

    if err := json.Unmarshal(awfulCase, &badInput); err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", badInput)

}

Not the most elegant way to do it, but it works :)

https://play.golang.org/p/dwAPmGtHzZi

Top comments (0)