Sunday, March 6, 2022

Custom Marshaling in GoLang


 


In this post we will review how to handle custom marshaling in GoLang.

I've recently had to marshal a structure containing a map with float as key. Then I got this error:


json: unsupported type: map[float64]int


Reading the documents I've found that since JSON does not support float as keys, the GoLang json marshaling does not automatically convert the float to string, and instead is returning an error. The solution in this case is to implement a custom marshaling. Let's examine the structures.


type MainStruct struct {
Name string
InnerData InnerStruct
}

type InnerStruct struct {
Exists bool
Count int
Values map[float64]int
}


We have a main struct, and an inner struct. I've included two structures to emphasis that the marshaling is done on the main struct, but still we will add our custom marshaling on the inner struct, and it will be used even that the marshaling is not done directly on it. Let's examine the marshal example:


func TestJson(t *testing.T) {
data := MainStruct{
Name: "john",
InnerData: InnerStruct{
Exists: true,
Count: 72,
Values: map[float64]int{
1.2: 42,
3.4: 56,
},
},
}

jsonBytes, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}

t.Log(string(jsonBytes))

var loadedData MainStruct
err = json.Unmarshal(jsonBytes, &loadedData)
if err != nil {
t.Fatal(err)
}

t.Log(loadedData)
}


If we will run the test now, we will get the unsupported type error displayed above. To solve the issue we add 2 methods to handle the custom marshaling and unmarshaling.


func (i InnerStruct) MarshalJSON() ([]byte, error) {
newStruct := struct {
Exists bool
Count int
Values map[string]int
}{
Exists: i.Exists,
Count: i.Count,
}

if i.Values != nil {
newStruct.Values = make(map[string]int)
for key, value := range i.Values {
keyString := fmt.Sprintf("%v", key)
newStruct.Values[keyString] = value
}
}
return json.Marshal(&newStruct)
}

func (i *InnerStruct) UnmarshalJSON(data []byte) error {
newStruct := struct {
Exists bool
Count int
Values map[string]int
}{}

err := json.Unmarshal(data, &newStruct)
if err != nil {
return fmt.Errorf("custom unmarshal failed: %v", err)
}

i.Exists = newStruct.Exists
i.Count = newStruct.Count

if newStruct.Values != nil {
i.Values = make(map[float64]int)
for key, value := range newStruct.Values {
valueFloat, err := strconv.ParseFloat(key, 64)
if err != nil {
return fmt.Errorf("parse float failed: %v", err)
}
i.Values[valueFloat] = value
}
}
return nil
}



The methods convert the map of floats keys to map of strings keys, and hence bypass the GoLang non-supporting the float as key. Both of the methods are using a temporary structure to covert the float to string and vise versa.


Final Note

In this post we have reviewed how to overcome the unsupported type error for GoLang marshaling. Note that other languages, such as javascript automatically handle this conversion.





 




No comments:

Post a Comment