Skip to content

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

Open

Description

@garrettladley

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.

Activity

  1. danhawkins commented on May 13, 2024

    @danhawkins

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

  2. sonu27 commented on Jul 26, 2024

    @sonu27

    +1 for this

  3. sagikazarmark commented on Aug 11, 2024

    @sagikazarmark
    Member

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

  4. sonu27 commented on Aug 13, 2024

    @sonu27

    @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 :)

  5. ederuiter commented on Sep 3, 2024

    @ederuiter

    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)
    }
  6. timnguyen3051234 commented on Nov 28, 2024

    @timnguyen3051234

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

  7. Bechma commented on Jan 29, 2025

    @Bechma

    The hook function from ederuiter is working.
    As I understand, this is a bug or an unsupported feature(for time.Time), right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions