elithrar
elithrar

Reputation: 24260

Decode JSON into Interface Value

As encoding/json needs a non-nil interface to unmarshal into: how can I reliably make a (full) copy of a user-provided pointer type, store that in my User interface, and then JSON decode into that ad-hoc?

Note: the goal here is to do this 'unattended' - that is, pull the bytes from Redis/BoltDB, decode into the interface type, and then check that the GetID() method the interface defines returns a non-empty string, with request middleware.

Playground: http://play.golang.org/p/rYODiNrfWw

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "time"
)

type session struct {
    ID      string
    User    User
    Expires int64
}

type User interface {
    GetID() string
}

type LocalUser struct {
    ID      string
    Name    string
    Created time.Time
}

func (u *LocalUser) GetID() string {
    return u.ID
}

type Auth struct {
    key []byte
    // We store an instance of userType here so we can unmarshal into it when
    // deserializing from JSON (or other non-gob decoders) into *session.User.
    // Attempting to unmarshal into a nil session.User would otherwise fail.
    // We do this so we can unmarshal from our data-store per-request *without
    // the package user having to do so manually* in the HTTP middleware. We can't
    // rely on the user passing in an fresh instance of their User satisfying type.
    userType User
}

func main() {
    // auth is a pointer to avoid copying the struct per-request: although small
    // here, it contains a 32-byte key, options fields, etc. outside of this example.
    var auth = &Auth{key: []byte("abc")}
    local := &LocalUser{"19313", "Matt", time.Now()}

    b, _, _, err := auth.SetUser(local)
    if err != nil {
        log.Fatalf("SetUser: %v", err)
    }

    user, err := auth.GetUser(b)
    if err != nil {
        log.Fatalf("GetUser: %#v", err)
    }

    fmt.Fprintf(os.Stdout, "%v\n", user)

}

func (auth *Auth) SetUser(user User) (buf []byte, id string, expires int64, err error) {
    sess := newSession(user)

    // Shallow copy the user into our config. struct so we can copy and then unmarshal
    // into it in our middleware without requiring the user to provide it themselves
    // at the start of every request
    auth.userType = user

    b := bytes.NewBuffer(make([]byte, 0))
    err = json.NewEncoder(b).Encode(sess)
    if err != nil {
        return nil, id, expires, err
    }

    return b.Bytes(), sess.ID, sess.Expires, err
}

func (auth *Auth) GetUser(b []byte) (User, error) {
    sess := &session{}

    // Another shallow copy, which means we're re-using the same auth.userType
    // across requests (bad).
    // Also note that we need to copy into it session.User so that encoding/json
    // can unmarshal into its fields.
    sess.User = auth.userType

    err := json.NewDecoder(bytes.NewBuffer(b)).Decode(sess)
    if err != nil {
        return nil, err
    }

    return sess.User, err
}

func (auth *Auth) RequireAuth(h http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        // e.g. user, err := store.Get(r, auth.store, auth.userType)
        // This would require us to have a fresh instance of userType to unmarshal into
        // on each request.

        // Alternative might be to have:
        // func (auth *Auth) RequireAuth(userType User) func(h http.Handler) http.Handler
        // e.g. called like http.Handle("/monitor", RequireAuth(&LocalUser{})(SomeHandler)
        // ... which is clunky and using closures like that is uncommon/non-obvious.
    }

    return http.HandlerFunc(fn)
}

func newSession(u User) *session {
    return &session{
        ID:      "12345",
        User:    u,
        Expires: time.Now().Unix() + 3600,
    }
}

Upvotes: 0

Views: 1355

Answers (2)

Thundercat
Thundercat

Reputation: 121009

Because the application will decode to a User and the argument to the JSON decoder must be a pointer value, we can assume that User values are pointer values. Given this assumption, the following code can be used to create a new zero value for decoding:

uzero := reflect.New(reflect.TypeOf(u).Elem()).Interface().(User)

playground example

Upvotes: 1

Zippo
Zippo

Reputation: 16420

If you need to deep copy an interface, add that method to your interface.

type User interface {
  GetID() string
  Copy() User
}

type LocalUser struct {
  ID string
  Name string
  Created time.Time
}

// Copy receives a copy of LocalUser and returns a pointer to it.
func (u LocalUser) Copy() User {
  return &u
}

Upvotes: 1

Related Questions