The Mystery Of JSON Conversion Of Int64 To Float64

Ahmad Helaly - Sep 1 - - Dev Community

Working with JSON can sound simple & clear, you have some struct, you can change it to JSON — A general unified language & back to your struct. Simple right? 🙂

Well, yeah, but that's until you encounter some weird behavior from the Marshal / Unmarshal functions.

Problem 😨

It all started when I was trying to read the encoded payload from a JWT token, below is an example that demonstrates the issue

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID      int64   `json:"id"`
    PostIDs []int64 `json:"post_ids"`
}

func main() {
    u := User{
        ID:      1,
        PostIDs: []int64{1, 2, 3},
    }

    b, err := json.Marshal(u)
    if err != nil {
        panic(err)
    }

    m := make(map[string]interface{})
    if err = json.Unmarshal(b, &m); err != nil {
        panic(err)
    }

    userID, ok := m["id"].(int64)
    fmt.Printf("id: %d\nOk:%t\n", userID, ok)

    fmt.Println() // spliter

    postIDs, ok := m["id"].([]int64)
    fmt.Printf("post_ids: %v\nOk:%t\n", postIDs, ok)
}
Enter fullscreen mode Exit fullscreen mode

Just marshaling and unmarshaling back out struct, so it's expected to return the same value!

Unfortunately, this didn't happen, the code above outputs

// Result
id: 0
Ok:false

post_ids: []
Ok:false
Enter fullscreen mode Exit fullscreen mode

Once I saw that output, I 🤔 the issue might be with type conversions, so I went to check what types does these interfaces have


    fmt.Printf("id: %T\n", m["id"])
    fmt.Printf("post_ids: %T\n", m["post_ids"])
Enter fullscreen mode Exit fullscreen mode
// Result
id: float64
post_ids: []interface {}
Enter fullscreen mode Exit fullscreen mode

So as we can see, JSON has parsed out int64 as float64, which lead to issues upon reading the data.

There is actually 2 ways to fix this issue

📃 Solution 01 (Hard way)

Use type assertions of float64, Notice that []interface{} can't be mapped right away to []float64, so we have to iterate each element and convert it

        // Parse UserID
    userID, _ := m["id"].(float64)
    fmt.Printf("id: %f\n", userID)

    fmt.Println() // spliter

    // Parse PostIDs
    postIDsArr, _ := m["post_ids"].([]interface{})
    postIDs := make([]int64, len(postIDsArr))
    for i, v := range postIDsArr {
        id, _ := v.(float64) // NOTICE: direct conversion to int64 won't work here!
        postIDs[i] = int64(id)
    }

    fmt.Printf("post_ids: %v\n", postIDs)
Enter fullscreen mode Exit fullscreen mode
// Result
id: 1.000000

post_ids: [1 2 3]
Enter fullscreen mode Exit fullscreen mode

📃 Solution 02 (Easy way)

Parse it back to a struct

    b, err = json.Marshal(m) // m = map[string]interface{}
    if err != nil {
        panic(err)
    }

    var u2 User
    if err := json.Unmarshal(b, &u2); err != nil {
        panic(err)
    }

    fmt.Println(u2.ID)
    fmt.Println(u2.PostIDs)
Enter fullscreen mode Exit fullscreen mode

Of course, you might think, why should we even use Solution 01, isn't Solution 02 better?

Well it depends, you don't always want to create a struct to read a single attribute from a struct, so the correct answer is -- It depends!

I think that's all for today's article, wish you learned something new, my fellow gopher 🥰.

.
Terabox Video Player