Akshay Deo

Do(nt) Code Seriously

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.

User model
1
2
3
4
5
6
7
8
9
10
11
12
  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 this repo : http://github.com/ottemo/mapstructure. This had everything I wanted. (Prefer this repo over https://github.com/mitchellh/mapstructure 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.

  • First we will convert the JSON into a map[string]interface{}
JSON to map[string]interface{}
1
2
3
4
5
  var data map[string]interface{}
  err = json.Unmarshal(body, &data)
  if err != nil {
    return nil, err
  }
  • Okay so data contains our JSON, now as we have a custom fields time.Time we will have to write our own decoder, which means create on AdvancedDecodeHook func and pass it to the DecoderConfig
AdvancedDecodeHook
1
2
3
4
5
6
7
8
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:

  • val => indicates type of the data
  • data => value of that data

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.

  • And now just write a function to return the decoder with our custom configuration
DecoderFactory
1
2
3
4
5
6
7
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.

  • And finally call it from your routine
Decode the JSON
1
2
3
4
5
6
7
8
  decoder, err := getDecoder(&user)
  if err != nil {
    return nil, err
  }
  err = decoder.Decode(data["user"])
  if err != nil {
    return nil, err
  }
  • And you are done.

Final code

So the final code looks like this

Converting JSON to Struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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