vendredi 1 septembre 2023

How to determine if struct property has `nil` value using reflection in Go?

I am creating an app in Go that is supposed to receive a JSON and unmarshal it into different possible values.

To do this, I created a parent struct, that contains the possible JSON formats that the app will receive.

So the idea is to try to unmarshal the received JSON into each child struct and check if any of them didn't receive any values. If this is the case, then the app will set that struct to nil at a later point.

The problem is that when I try to use the isNil() method from Go's reflection, it doesn't work for a generic struct. It only works if I know the struct's format.

Here is an example (playground link):

package main

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

type User struct {
    Username *string `json:"username,omitempty" required:"true"`
    Password *string `json:"password,omitempty" required:"false"`
}

type UserError struct {
    Error *string `json:"error,omitempty" required:"true"`
    Code  *int    `json:"code,omitempty" required:"true"`
}

type UserOrError struct {
    User      *User
    UserError *UserError
}

var userJson = `{ "username": "john@email.com", "password": "password" }`

func main() {
    user := &UserOrError{}
    data := []byte(userJson)

    types := reflect.TypeOf(user).Elem()
    values := reflect.ValueOf(user).Elem()

    for i := 0; i < types.NumField(); i++ {
        fieldType := types.Field(i)
        unmarshalledValue := reflect.New(fieldType.Type)

        err := json.Unmarshal(data, unmarshalledValue.Interface())
        if err != nil {
            panic(err)
        }

        value := unmarshalledValue.Elem()
        fmt.Printf("Unmarshalled: %+v - hasValues: %v \n", value, hasValues(value))
        // Unmarshalled: &{Username:0x14000102110 Password:0x14000102120} - hasValues: true
        // Unmarshalled: &{Error:<nil> Code:<nil>} - hasValues: true
        values.Field(i).Set(value)
    }

    fmt.Printf("User: %+v - hasValues: %v\n", user.User, hasValues(user.User))
    // User: &{Username:0x14000102110 Password:0x14000102120} - hasValues: true
    fmt.Printf("UserError: %+v - hasValues: %v\n", user.UserError, hasValues(user.UserError))
    // UserError: &{Error:<nil> Code:<nil>} - hasValues: false
}

func hasValues(obj any) bool {
    var values reflect.Value
    if reflect.TypeOf(obj).Kind() == reflect.Ptr {
        values = reflect.ValueOf(obj).Elem()
    } else {
        values = reflect.ValueOf(obj)
    }

    for i := 0; i < values.NumField(); i++ {
        innerValue := values.Field(i)
        if innerValue.Kind() == reflect.Ptr && !innerValue.IsNil() {
            return true
        } else if !innerValue.IsZero() {
            return true
        }
    }

    return false
}

As you can see, isNil() works if I know the struct beforehand, so hasValues is false for the UserError struct, but if I call the same function using the generic struct that I created through reflect.New (unmarshalledValue), then hasValues always returns true.

What am I doing wrong here?





Aucun commentaire:

Enregistrer un commentaire