I’m running into an issue where I register some type codecs and things almost work 100% as expected, except for one case. In the code below, the Decimal type is a pointer that stores a number. For reads, I’m using a custom decoder for the Decimal type to convert the bson Decimal128 to a Decimal and it’s working correctly. For writes, I’m using a custom encoder. It’s working except when the number value of the Decimal is 0. In the code snippet below, update1 works correctly and ServiceFee of the order is updated to the new value. However if I try to run update0 and set the value to 0, the update operation succeeds but the value of ServiceFee is not updated. With some logging enabled, I can also see that the registered encoder is NOT being called when running update0 . Since ServiceFee on update0 is of type *Decimal, I would expect the encoder to run; it shouldn’t matter that bson omitempty is applied to the ServiceFee field on OrderUpdate type because ServiceFee is NOT empty, it’s a non-nil pointer to a value. But it looks like mongo driver is seeing the *Decimal with underlying value of 0 as an empty value and omitting it.
package main
import (
"encoding/json"
"reflect"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/shopspring/decimal"
)
type Decimal decimal.Decimal
type Order struct {
Id string `bson:"_id" json:"id"`
ServiceFee *Decimal `bson:"svcFee" json:"serviceFee"`
}
type OrderUpdate struct {
Id string `bson:"-" json:"id"`
ServiceFee *Decimal `bson:"svcFee,omitempty" json:"serviceFee,omitempty"`
}
func createCustomRegistry() *bsoncodec.RegistryBuilder {
var primitiveCodecs mongoBson.PrimitiveCodecs
rb := bsoncodec.NewRegistryBuilder()
bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb)
bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb)
var dec *Decimal
decimalType := reflect.TypeOf(dec)
rb.RegisterTypeDecoder(
decimalType,
bsoncodec.ValueDecoderFunc(func(_ bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if vr.Type() == bsontype.Null {
err := vr.ReadNull()
if err != nil {
return err
}
var emptyDecimal *Decimal
val.Set(reflect.ValueOf(emptyDecimal))
return nil
} else {
read, err := vr.ReadDecimal128()
if err != nil {
return err
}
decimalValue, _ := NewDecimalFromString(read.String())
val.Set(reflect.ValueOf(decimalValue))
return nil
}
}),
)
rb.RegisterTypeEncoder(
decimalType,
bsoncodec.ValueEncoderFunc(func(_ bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
decimalValue := val.Interface().(*Decimal)
nextValue, _ := primitive.ParseDecimal128(decimalValue.GetDecimalString())
return vw.WriteDecimal128(nextValue)
}),
primitiveCodecs.RegisterPrimitiveCodecs(rb)
return rb
}
func CreateClient(host, user, pwd string) (*MongoClient, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
uri := fmt.Sprintf("mongodb://%s:27017", host)
var clientOpts *options.ClientOptions
if user != "" {
credentials := options.Credential{
Username: user,
Password: pwd,
}
clientOpts = options.Client().ApplyURI(uri).
SetAuth(credentials)
} else {
clientOpts = options.Client().ApplyURI(uri)
}
registry := createCustomRegistry().Build()
clientOpts.SetRegistry(registry)
client, err := mongo.Connect(ctx, clientOpts)
if err != nil {
log.Println(err)
}
return client, err
}
func UpdateOrder(client data *OrderUpdate) (order *Order, err error) {
db := client.Database("orders_db")
ctx := context.TODO()
collection := db.Collection("orders")
query := bson.M{"_id": data.Id}
out := &Order{}
updateQuery := bson.M{"$set": data}
err = collection.FindOneAndUpdate(
ctx,
query,
updateQuery,
options.FindOneAndUpdate().SetReturnDocument(options.After),
).Decode(&out)
return out, err
}
client, _ := CreateClient("my-mongo-uri" "my-mongo-user", "my-mongo-pass")
orderId := "some-id"
serviceFee0 := 0
serviceFee1 := 1
update1 := &OrderUpdate{
Id: orderId,
ServiceFee: Decimal(decimal.New(int64(serviceFee1), 0))
}
// Works
result1, err1 := UpdateOrder(update1)
update0 := &OrderUpdate{
Id: orderId,
ServiceFee: Decimal(decimal.New(int64(serviceFee0), 0))
}
// Does not work
result0, err0 := UpdateOrder(update0)
Any suggestions are welcome, thanks!