map to struct in Golang


Recently I was working with a piece of code where I had to convert a JSON into a Golang struct. I faced hell lot of issues, and waster a bit of time in achieving that, so thought of documenting it.

Scenario

I have a struct called User, which has fields. There were no issues with primary data types, but when it came to time.Time, it started fucking me.

  Id      int    `json:"user_id"`
  AuthKey string `json:"-" sql:"not null;unique"`
  
  Name         string `json:"user_name" sql:"not null;unique"`
  EmailAddress string `json:"email_address" sql:"not null;unique"`
  Password     string `json:"-" sql:"not null"`

  AccountId int `json:"account_id" sql:"not null;unique"`

  CreatedAt time.Time `json:"created_at"`
  UpdatedAt time.Time `json:"updated_at"`
  DeletedAt time.Time `json:"deleted_at"`

A bit of googling landed me onto http://github.com/ottemo/mapstructure. This had everything I wanted. (Prefer https://github.com/mitchellh/mapstructure repo over as ottemo/mapstructure has an AdvancedDecodeHook that helps a bit in this situation)

Solution

Suppose that body has the entire JSON. It has some fields along with User struct with user as key.

  var data map[string]interface{}
  err = json.Unmarshal(body, &data)
  if err != nil {
    return nil, err
  }
func myDecoder(val *reflect.Value, data interface{}) (interface{}, error) {
  if val.Type().String() == "time.Time" {    
    value, err := time.Parse(time.RFC3339Nano, data.(string))
    val.Set(reflect.ValueOf(value))
    return nil, err
  }
  return data, nil
} 

So lets understand the structure of this hook - You get two parameters:

This function returns an interface and an error. Now this AdvancedDecodeHook works in a way that if you return nil in place of interface, it the decoder assumes that our custom decoder has parsed the value for the given data, and it leaves it in that way. In case of error the entire parsing fails. So you have to decide on whether to throw the error or set a default value when parsing fails.

func getDecoder(result interface{}) (*mapstructure.Decoder, error) {
  return mapstructure.NewDecoder(&mapstructure.DecoderConfig{
    AdvancedDecodeHook: myDecoder,
    TagName:            "json",
    Result:             result,
    WeaklyTypedInput:   false})
}

The parameter that we pass in is the result type we expect. This has to be the pointer to the struct.

  decoder, err := getDecoder(&user)
  if err != nil {
    return nil, err
  }
  err = decoder.Decode(data["user"])
  if err != nil {
    return nil, err
  }

Final code

So the final code looks like this

func myDecoder(val *reflect.Value, data interface{}) (interface{}, error) {
  if val.Type().String() == "time.Time" {    
    value, err := time.Parse(time.RFC3339Nano, data.(string))
    val.Set(reflect.ValueOf(value))
    return nil, err
  }
  return data, nil
} 

func getDecoder(result interface{}) (*mapstructure.Decoder, error) {
  return mapstructure.NewDecoder(&mapstructure.DecoderConfig{
    AdvancedDecodeHook: myDecoder,
    TagName:            "json",
    Result:             result,
    WeaklyTypedInput:   false})
}

func GetUserFromJSON(jsonUser string) (*User,error){
  var data map[string]interface{}
  err = json.Unmarshal(jsonUser, &data)
  if err != nil {
    return nil, err
  }
  decoder, err := getDecoder(&user)
  if err != nil {
    return nil, err
  }
  err = decoder.Decode(data["user"])
  if err != nil {
    return nil, err
  }
  return &user,nil
}

Thank you guys, keep coding :)


Comments