0

I'm calling an external API that i'm unmarshalling into a struct.

In the response most fields are integer but as it's json there are several edge cases where it could return a string but still be a valid / useful information : "NaN" , "N/A"

My struct is looks like this :

type Example struct {
  Field1 *int64 `json:"field_1,omitempty"`
  Field2 *int64 `json:"field_2,omitempty"`
  Field3 *int64 `json:"field_3,omitempty"`
}

We have several requirement :

  • If the api returns NaN or N/A I should display an error to my user in the FE so I'm thinking to replace the values with null while "catching" the error beforehand that's why I've chosen a pointer value.

  • If no value is returned , omit the value altogether when re-marshalling.

In order to do so I'm trying to replace the "NaN" value with JSON null doing

 b = bytes.Replace(b, []byte("NaN"), []byte("null"), -1) ` 

but it doesn't work as "null" is not equal to null and that's problem number 1.

2nd problem is that the omitempty also doesn't distinguish between nil, 0 and empty values when remarshalling.

So the remarshalling also fails. I know it's a "common" problem in go that is being fixed but is there a work around for now?

Because if I pass nil for " N/A " and "NaN" and use omitempty it will remove them. If I pass 0 it won't make sense ( business wise as 0 have meaning other than "not initialized" ) and if I remove Omitempty it will have the whole struct marshalled everytime ( lots of unnecessary data ) and no way to differentiate between nil ( NA / NaN ) and nil ( no value ).

Last option would be to build a custom type and marshall / unmarshaller like this :

type JSONint64 struct {
  value *int64
  error string
}

but that would require me to check every number in my json response , every time , when in fact NaN and N/A are very rare occurrences and adds " complexity " on the front end.

I'm assuming it's a common problem as JSON is untyped , how is this generally fixed ?

Stephen
  • 8,508
  • 12
  • 56
  • 96
  • Or maybe you can unmarshal into an interface and then check if value is integer – Shubham Srivastava Sep 04 '20 at 11:28
  • but that would force me to assert for each field ( there's alot ) every time and would reduce performance by a lot i think – golestion Sep 04 '20 at 11:50
  • Check this discussion https://stackoverflow.com/questions/28024884/does-a-type-assertion-type-switch-have-bad-performance-is-slow-in-go – Shubham Srivastava Sep 04 '20 at 12:03
  • What exactly is the problem with using a custom unmarshaler? What do you mean by *"that would require me to check every number in my json response , every time "*? Since you're using pointers you still have to check them for `nil`, and dereference them whenever you want to do something useful with them, like math for example. Or am I missing something? – mkopriva Sep 04 '20 at 12:30
  • Yes it's unclear. What i mean is that to validate that i'm receiving valid data i would have to run the type assertion function on all value i'm receiving instead of just naturally unmarshalling them into the *int64 type and treating the few edge case if there's an error. I'm not ever receiving null just sending it back to my front end , that the second part of the question where it's hard in Go to differenciat between nil value and Empty value for pointer. – golestion Sep 04 '20 at 13:36

1 Answers1

0

I would Unmarshal to a map[string]interface{} value and then use reflection or type assertion to figure out the data type of the values.

Unmarshal stores one of these in the interface value:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

Like so:

package main

import (
    "encoding/json"
    "fmt"
)

type Example struct {
    Field1 *int64 `json:"field_1,omitempty"`
    Field2 *int64 `json:"field_2,omitempty"`
    Field3 *int64 `json:"field_3,omitempty"`
}

func typeof(v interface{}) string {
    switch v.(type) {
    case float64:
        return "float64"
    case string:
        return "string"
    default:
        return "unknown"
    }
}

func main() {
    d := []byte(`{"field_1": "n/a", "field_2": 2 }"`)
    e := make(map[string]interface{})
    err := json.Unmarshal(d, &e)
    if err != nil {
        fmt.Printf("%v\n", err)
    }
    fmt.Printf("%+v\n", e)
    example := Example{}
    for k, v := range e {
        switch typeof(v) {
        case "float64":
            val := int64(v.(float64))
            switch k {
            case "field_1":
                example.Field1 = &val
            case "field_2":
                example.Field2 = &val
            case "field_3":
                example.Field3 = &val
            default:
                fmt.Printf("Unexpected field: %v\n", k)
            }

        default:
            // display error
        }
    }
    fmt.Printf("Example field_2: %d\n", *example.Field2)

}
alessiosavi
  • 2,753
  • 2
  • 19
  • 38
Eelco
  • 507
  • 3
  • 18
  • but wouldn't that be extremely slow ? i'm often reading not to use type asseration in Go. I'm trying to avoid it as the NaN or N/A are very very rare in my use case. So i would be running something very slow just for very rare edge cases. That's why i was thinking about just replacing them with null using something like Byte.replace() – golestion Sep 04 '20 at 13:34
  • If that is an issue you might want to write a benchmark test comparing the options. – Eelco Sep 04 '20 at 14:06