Search code examples
jsongomarshalling

Go How to deal with float infinity before converting to JSON


I've come across a situation where I have some float64 fields that could be infinity/NaN and trying to marshal to JSON would result in an error regarding +Inf type isn't supported.

type Something interface {
  Id string `firestore:"id"`
  NumberA float64 `firestore:"numberA"`
  NumberB float64 `firestore:"numberB"`
  NumberC float64 `firestore:"numberC"`
}

This struct gets initially populated via another library (Google Firestore).

In reality this struct is much larger with a lot more fields that are floats.

I think I could use something like this loop below using reflect to to find them all, though I wonder if there is a cleaner way or more idiomatic approach.

v := reflect.ValueOf(structVar)
typeOfS := v.Type()
for i := 0; i< v.NumField(); i++ {
  if typeOfS.Field(i).Type.Kind() == reflect.Float64 && math.IsInf(v.Field(i).Interface().(float64), 1) {
    // ... some logic I'll put here
  }
}

I don't understand how to implement custom marshalling so maybe that could be an option to handle +Inf?


Solution

  • Custom handling of values can be done through custom types that implement the Marshaler interface. Your Something type, though, is malformed. It's defined as type Something interface{}, whereas that really ought the be a type Something struct:

    type Something struct {
        Id      string    `firestore:"id"`
        NumberA JSONFloat `firestore:"numberA"`
        NumberB JSONFloat `firestore:"numberB"`
        NumberC JSONFloat `firestore:"numberC"`
    }
    
    type JSONFloat float64
    
    func (j JSONFloat) MarshalJSON() ([]byte, error) {
        v := float64(j)
        if math.IsInf(j, 0) {
            // handle infinity, assign desired value to v
            // or say +/- indicates infinity
            s := "+"
            if math.IsInf(v, -1) {
                s = "-"
            }
            return []byte(s), nil
        }
        return json.Marshal(v) // marshal result as standard float64
    }
    
    func (j *JSONFloat) UnsmarshalJSON(v []byte) error {
        if s := string(v); s == "+" || s == "-" {
            // if +/- indiciates infinity
            if s == "+" {
                *j = JSONFloat(math.Inf(1))
                return nil
            }
            *j = JSONFloat(math.Inf(-1))
            return nil
        }
        // just a regular float value
        var fv float64
        if err := json.Unmarshal(v, &fv); err != nil {\
            return err
        }
        *j = JSONFloat(fv)
        return nil
    }
    

    That should do it