Search code examples
gounmarshalling

Go - Custom unmarshaler - string to float


I consume a 3th party API that gives me the price as a string, there are 3 flavours:

//Input
[
    {"id": "A", "price": "123"},
    {"id": "B", "price": "123.5"},
    {"id": "C", "price": ""}
]

//Expected output

[
    A 123,
    B 123.5,
    C 0
]

I know i can add the ,string part inside my struct but this will not handle order C. I have found this github issue where this question is asked about int's. In there there is a playground code that i altered a little bit to my needs.

If i change this to float64 instead of int i get an error with the log of order A that is now 0.

json: cannot unmarshal string into Go struct field Order.price of type float64
[{A 0}]

I thought that this if string(data) == '""' { part of the code will check for the empty string (case order C) if so: set to 0. But "123" is not "". The rest of the custom unmarshaler i understand apart from the part below. Can someone point me to the right direction?

p := (*float64)(foe)
*p = f

The full code Also here

package main

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

type StringToFloat float64

type Order struct {
    Id    string        `json:"id"`
    Price StringToFloat `json:"price"`
}

func (foe *StringToFloat) UnmarshalJSON(data []byte) error {
    if string(data) == `""` {
        if foe != nil {
            *foe = 0
        }
        return nil
    }

    var f float64
    err := json.Unmarshal(data, &f)
    if err != nil {
        return err
    }
    p := (*float64)(foe)
    *p = f
    return nil
}

func main() {
    jsonString := `[
        {"id": "A", "price": "123"},
        {"id": "B", "price": "123.5"},
        {"id": "C", "price": ""}
    ]`
    var orders []Order
    if err := json.NewDecoder(strings.NewReader(jsonString)).Decode(&orders); err != nil {
        fmt.Println(err)
    }

    fmt.Println(orders)
}

EDIT: comment @mkopriva

I adjusted the unmarshaler:

func (foe *StringToFloat) UnmarshalJSON(data []byte) {
    fmt.Println(string(data))
    if string(data) == `""` {
        if foe != nil {
            *foe = 0
        }
    }

    n, err := strconv.ParseFloat(string(data), 64)
    if err != nil {
        fmt.Println(err)
    }

    *foe = StringToFloat(n)
}

What gives me this output:

json: cannot unmarshal string into Go struct field Order.price of type main.StringToFloat
[{A 0} {B 0} {C 0}]

Solution

  • Try this

    package main
    
    import (
        "encoding/json"
        "fmt"
        "strconv"
        "strings"
    )
    
    type StringToFloat float64
    
    type Order struct {
        Id    string        `json:"id"`
        Price StringToFloat `json:"price"`
    }
    
    func (foe *StringToFloat) UnmarshalJSON(data []byte) error {
        if string(data) == "\"\"" {
            if foe != nil {
                *foe = 0
            }
            return nil
        }
        num := strings.ReplaceAll(string(data), "\"", "")
        n, err := strconv.ParseFloat(num, 64)
        if err != nil {
            return err
        }
        *foe = StringToFloat(n)
        return nil
    }
    
    func main() {
        jsonString := `[
            {"id": "A", "price": "123"},
            {"id": "B", "price": "123.5"},
            {"id": "C", "price": ""}
        ]`
        var orders []Order
        if err := json.NewDecoder(strings.NewReader(jsonString)).Decode(&orders); err != nil {
            fmt.Println(err)
        }
    
        fmt.Println(orders)
    }
    

    Go playground: https://go.dev/play/p/cHjNQ447eX9

    Things to note:

    • The function signature of UmarshalJSON func needs to be exactly the same as given to be picked up by Decode. In your updated code error return type was missing
    • string(data) gives "123" so the quotes need to be removed before ParseFloat
    • return nil is required after string = "" check so it dosent go further