0

I've got a Set function that wraps a users object (or variable) in my own struct called sessions. It assigns it to the Value field of my sessions struct. The Set function then marshalls this struct and assigns the string somewhere in storage.

My problem is that I'm not sure how to implement my Get function to only return the unmarshalled struct stored in the Value field, opposed to the entire sessions wrapper struct.

I've made a very simple example demonstrating what I'm talking about.

I can't use a type assertion in the assignment in my Get func because I don't know what type the user is going to use in advance.

I suspect there may be a way using reflection to accomplish this?

Edit: The two provided answers so far are not what I'm looking for. I do not know what type the user will be using, it could be anything, so coding around that by hard coding their type or trying to "guess" what it may contain is not going to work.

b0xxed1n
  • 2,063
  • 5
  • 20
  • 30

3 Answers3

1

OK, I think I know what you're wanting to do. I found this answer Converting map to struct and made some tweaks to get it working for your particular use case. Note: this hasn't been tested thoroughly and may be a little shaky, use at your own risk:

package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "reflect"
)

type session struct {
    Value interface{}
    Flash map[string]string
}

type Person struct {
    Name string
    Age  int
}

func Get(pointer interface{}) {
    marshalledString := `{"Value":{"Name":"bob","Age":3},"Flash":null}`

    var sess session

    d := json.NewDecoder(bytes.NewBuffer([]byte(marshalledString)))
    d.UseNumber()
    if err := d.Decode(&sess); err != nil {
        panic(err)
    }

    fmt.Printf("%#v", sess)

    switch sess.Value.(type) {
    case map[string]interface{}:
        err := FillStruct(sess.Value.(map[string]interface{}), pointer)
        if err != nil {
            log.Fatal(err)
        }
    default:
        return // You may want to return an error here...
    }
}

func main() {
    var personObj Person

    Get(&personObj)

    // Wanting to see personObj here have Name "bob" and Age 3
    fmt.Printf("%#v", personObj)
}

func SetField(obj interface{}, name string, value interface{}) error {
    structValue := reflect.ValueOf(obj).Elem()
    structFieldValue := structValue.FieldByName(name)

    if !structFieldValue.IsValid() {
        return fmt.Errorf("No such field: %s in obj", name)
    }

    if !structFieldValue.CanSet() {
        return fmt.Errorf("Cannot set %s field value", name)
    }

    structFieldType := structFieldValue.Type()
    val := reflect.ValueOf(value)

    if _, ok := value.(json.Number); ok {
        if f, err := value.(json.Number).Int64(); err == nil {
            structFieldValue.SetInt(f)
            return nil
        }
        if f, err := value.(json.Number).Float64(); err == nil {
            structFieldValue.SetFloat(f)
            return nil
        }
    }

    if structFieldType != val.Type() {
        return errors.New(fmt.Sprintf("Provided value type [%s] didn't match obj field type [%s]", val.Type().String(), structFieldType.String()))
    }

    structFieldValue.Set(val)
    return nil
}

func FillStruct(m map[string]interface{}, s interface{}) error {
    for k, v := range m {
        err := SetField(s, k, v)
        if err != nil {
            return err
        }
    }
    return nil
}
Community
  • 1
  • 1
jste89
  • 426
  • 4
  • 15
  • Thanks. This is one solution, although I'm not sure how robust it is (especially if you're having to deal with nested structs and so forth). What I've decided to do is just store the users struct as a json.RawMessage instead of an interface{}, to delay the decoding, and then I decode twice instead of once. The second decode can now decode the RawMessage directly into the users pointer. – b0xxed1n Dec 16 '16 at 01:19
0

The user may be able to pass in any value, but your code can deal with invalid input by passing an error back to them. If you know the desired format of the incoming data you can directly unmarshal it and handle any invalid input separately. This removes the need to have the intermediate interface{} that's hard to deal with:

https://play.golang.org/p/VNCflbk3GL

package main

import (
    "encoding/json"
    "fmt"
)

type session struct {
    Value Person
    Flash map[string]string
}

type Person struct {
    Name string
    Age  int
}

func Get(marshaled string) (Person, error) {
    var sess session
    err := json.Unmarshal([]byte(marshaled), &sess)
    if err != nil {
        return Person{}, err
    }
    fmt.Println(sess) // {{bob 3} map[]}
    return sess.Value, nil
}

func main() {
    person, err := Get(`{"Value":{"Name":"bob","Age":3},"Flash":null}`)
    if err != nil {
        fmt.Println("Got err:", err)
    }
    fmt.Printf("%#v", person) // main.Person{Name:"bob", Age:3}
}

If it's valid for Value to be multiple types, then you will have to do a type assertion somewhere. In Go it's not all that painful though:

https://newfivefour.com/golang-interface-type-assertions-switch.html

switch v := anything.(type) {
case string:
    fmt.Println(v)
case int32, int64:
    fmt.Println(v)
case SomeCustomType:
    fmt.Println(v)
default:
    fmt.Println("unknown")
}
Omar
  • 1,329
  • 11
  • 9
  • I don't know the inbound type unfortunately, which is why I have this problem. The type being passed in can be anything, it can be a struct, or it can be a variable. It's something the user defines themselves (this is for a web framework library). – b0xxed1n Dec 15 '16 at 10:15
0

You problem is that your incoming data type of Value is map[string]interface{}, and there's no direct/native way in Go to convert map into your type (while there's definitely code out there).

OK. If we assume that we totally have no control over incoming data in the Value field, but still, we can identify data type by a combination of its attributes, right? Because by definition, you should know possible options. We can create a universal incoming object instead of interface{}. AWS is using similar approach in their Go SDK, at least for DynamoDB service, setting optional attributes via pointers: https://github.com/aws/aws-sdk-go/blob/master/service/dynamodb/examples_test.go#L32

So, the approach is: your UnknownObj struct will have optional attributes that may be filled (and may be not) on json.Unmarshal. Knowing what fields were delivered via the switch, you can guess the data sent.

package main

import (
    "encoding/json"
    "fmt"
)

type session struct {
    Value UnknownObj
    Flash map[string]string
}

type UnknownObj struct {
    Name           *string
    Age            *float64
    SomeOtherField *map[string]string
}

func Get() UnknownObj {
    marshalledString := `{"Value":{"Name":"bob","Age":3},"Flash":null}`

    var sess session
    json.Unmarshal([]byte(marshalledString), &sess)
    return sess.Value
}

func main() {

    v := Get()

    switch {
    case v.Name != nil && v.Age != nil:
        fmt.Println("This is a Person")
    default:
        fmt.Println("Unknown data type")

    }

}

However, if you have control over the root/Values field and you can request to send you specific fields for each of the types instead of pushing all under Values, then you could have:

type session struct {
    Person   *Person
    Car      *Car
    Building *Buidling
    Etc      *Etc

    ...
}

This way, your solution will be even easier -> you'll just need to check what property is not nil.

Hope this helps.

Cheers.

Update Dec 15, 2016 To reply on your comment regarding the framework: what you are describing is a process of binding of user's request to an arbitrary data-type. OK. Unfortunately, its too much code to post here, but here's a link as a starting point: https://github.com/go-playground/validator/blob/v8.18.1/validator.go#L498

This is a package and approach Gin framework is using for binding here: https://github.com/gin-gonic/gin/blob/master/binding/json.go

Good luck!

oharlem
  • 1,030
  • 7
  • 12
  • Unfortunately I have no idea at all what they could be passing in. Whatever it is they're passing in is user defined, and can be literally anything (a struct, a variable, something with 1,000 layers of nesting, and so forth). This is for a web framework library, so the user has the ability to define any type of object to store in their session. – b0xxed1n Dec 15 '16 at 10:19
  • @b0xxed1n Got it. Please check my Update above! – oharlem Dec 15 '16 at 15:45