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!