Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode: time.Time field of input struct decoded to the zero value of time.Time in output struct #20

Open
garrettladley opened this issue May 13, 2024 · 6 comments

Comments

@garrettladley
Copy link

Repost of The time.Time type is converted to empty map from github.com/mitchellh/mapstructure.

When using Decode, the fields of type time.Time from the input struct are being decoded to the zero value of time.Time, time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), in the output struct.

@danhawkins
Copy link

I'd like to see the default behaviour here changed also, using the standard RFC3339 or handling time stuct mapping would be ideal

@sonu27
Copy link

sonu27 commented Jul 26, 2024

+1 for this

@sagikazarmark
Copy link
Member

Are you asking for a decode hook that converts a string that contains a RFC3339 timestamp to time.Time?

@sonu27
Copy link

sonu27 commented Aug 13, 2024

@sagikazarmark I can't speak for everyone, but it would be nice if mapstructure decoded time.Time correctly by default, i.e. no additional config. Most efficient implementation, up to you :)

@ederuiter
Copy link

Not the prettiest code, but atleast it works now .. the non-typed nil return took me a while to figure out

package main

import (
	"database/sql"
	"fmt"
	"github.com/thisisdevelopment/go-dockly/xerrors/iferr"
	"reflect"
	"time"

	"github.com/go-viper/mapstructure/v2"
	"gorm.io/gorm"
)

func decodeTime(mapData map[string]interface{}) (*time.Time, error) {
	to := &time.Time{}
	json, ok := mapData["_json"]
	if !ok {
		return nil, nil //TODO: error
	}
	err := to.UnmarshalJSON(json.([]byte))
	if to.IsZero() {
		return nil, nil
	}
	return to, err
}

func encodeTime(data *time.Time) (map[string]interface{}, error) {
	if data == nil {
		return map[string]interface{}{"_json": []uint8("null")}, nil
	}
	json, err := data.MarshalJSON()
	return map[string]interface{}{"_json": json}, err
}

func main() {

	toTime := func() mapstructure.DecodeHookFunc {
		timePtrType := reflect.TypeOf(&time.Time{})
		timeType := reflect.TypeOf(time.Time{})
		strMapType := reflect.TypeOf(map[string]interface{}{})
		sqlNullTimeType := reflect.TypeOf(sql.NullTime{})
		sqlNullTimePtrType := reflect.TypeOf(&sql.NullTime{})
		gormDeletedAtType := reflect.TypeOf(gorm.DeletedAt{})
		gormDeletedAtPtrType := reflect.TypeOf(&gorm.DeletedAt{})
		return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
			//for struct => struct conversions mapstructure currently employs the tactic to always decode it to a map
			//and then convert the map to the new struct. For the time.Time struct this fails as the time struct has no
			//public fields.
			//To work around this we json encode/decode the time
			//We also support sql.NullTime and gorm.DeletedAt
			if t == strMapType {
				if f == timePtrType {
					return encodeTime(data.(*time.Time))
				} else if f == timeType {
					d := data.(time.Time)
					return encodeTime(&d)
				} else if f == sqlNullTimeType {
					d := data.(sql.NullTime)
					if d.Valid {
						return encodeTime(&d.Time)
					} else {
						return encodeTime(nil)
					}
				} else if f == gormDeletedAtType {
					d := data.(gorm.DeletedAt)
					if d.Valid {
						return encodeTime(&d.Time)
					} else {
						return encodeTime(nil)
					}
				} else if f == sqlNullTimePtrType {
					d := data.(*sql.NullTime)
					if d.Valid {
						return encodeTime(&d.Time)
					} else {
						return encodeTime(nil)
					}
				} else if f == gormDeletedAtPtrType {
					d := data.(*gorm.DeletedAt)
					if d.Valid {
						return encodeTime(&d.Time)
					} else {
						return encodeTime(nil)
					}
				}
			}

			if f == strMapType && (t == sqlNullTimeType || t == sqlNullTimePtrType || t == timeType || t == timePtrType) {
				to, err := decodeTime(data.(map[string]interface{}))
				if err != nil {
					return nil, err
				}

				if t == timeType {
					if to == nil {
						//as we cannot return nil, return zero time instead
						return time.Time{}, nil
					}
					return *to, nil
				} else if t == timePtrType {
					if to == nil {
						//we can't just return to as to is a typed nil; currently mapstructure does not handle that well
						return nil, nil
					} else {
						return to, nil
					}
				} else if t == sqlNullTimeType {
					if to == nil {
						return sql.NullTime{Time: time.Time{}, Valid: false}, nil
					} else {
						return sql.NullTime{Time: *to, Valid: true}, nil
					}
				} else if t == sqlNullTimePtrType {
					if to == nil {
						return &sql.NullTime{Time: time.Time{}, Valid: false}, nil
					} else {
						return &sql.NullTime{Time: *to, Valid: true}, nil
					}
				}
			}

			return data, nil
		}
	}

	type From struct {
		Name      string
		CreatedAt time.Time
		DeletedAt gorm.DeletedAt
	}

	type To struct {
		Name      string
		CreatedAt time.Time
		DeletedAt *time.Time
	}

	input := &From{Name: "Test", CreatedAt: time.Now()}
	result := new(To)
	var decoder, _ = mapstructure.NewDecoder(&mapstructure.DecoderConfig{Squash: true, DecodeHook: toTime(), Result: result})
	err := decoder.Decode(input)
	iferr.Exit(err)
	fmt.Printf("%+v\n", result)
}

@timnguyen3051234
Copy link

I have the same problem. I tried some hook functions, but it still did not work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants