mongodbgounmarshallingbsonmongo-go

How to convert double to a time.Time value with the mongo Golang driver?


I've got a database with some legacy data where some of the documents store the field "createdDate" as a normal bson date, but others store the field as an epoch millisecond timestamp with the bson double type. (I know it's weird, but that's the legacy data I'm dealing with)

I tried reading it from the database using the standard approach via mongo.Connect in the go.mongodb.org/mongo-driver/mongo package, but the Decode function fails and returns error decoding key createdDate: cannot decode double into a time.Time.

Is there some way to tell the driver to read bson doubles as epoch millisecond timestamps when it goes to unmarshal the data into a struct with an associated time.Time field?

Sample data to seed:

{
    "_id": 1, 
    "createdDate": float64(1271858231000)
}

Sample struct to unmarshal the data into:

type Dummy struct {
    Id          int        `bson:"_id"`
    CreatedDate *time.Time `bson:"createdDate"`
}

Note: I'm using v1.16.0 of the mongo driver at this time.


Solution

  • You'll have to define a custom type decoder and add it to the bsoncodec registry that your mongo client uses for encoding/decoding data. There is a great overview of that process here.

    For your specific use case, the following decoder function should resolve the issue:

    var defaultTimeCodec = bsoncodec.NewTimeCodec()
    var typeTime = reflect.TypeOf(time.Time{})
    
    // DoubleToDateConverter converts bson doubles to time.Time
    func DoubleToDateConverter(ctx bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
        if !val.CanSet() || val.Type() != typeTime {
            return errors.New("bad type or not settable")
        } else if vr.Type() == bson.TypeDouble {
            epochMilliVal, err := vr.ReadDouble()
            if err != nil {
                return err
            }
            timeVal := time.Unix(0, int64(epochMilliVal)*int64(time.Millisecond))
            val.Set(reflect.ValueOf(timeVal))
            return nil
        }
    
        // run the default codec if the value being read from the db is not a double
        return defaultTimeCodec.DecodeValue(ctx, vr, val)
    }
    

    You can place the above wherever you set up your mongo client and apply it to the client's registry like so:

    registry := bson.NewRegistry()
    registry.RegisterTypeDecoder(typeTime, bsoncodec.ValueDecoderFunc(DoubleToDateConverter))
    
    clientOpts := options.Client().
        ApplyURI(uri).
        SetRegistry(registry)
      
    client, err := mongo.Connect(context.TODO(), clientOpts)
    // etc...
    

    Note: The bsoncodec.NewTimeCodec() function will be removed in the mongo Golang driver v2.0, but it's not clear yet what the replacement will be since the golang mongo client seems to be using it too.