13

I have a number of structs that require custom marshalling. When I was testing I was using JSON and the standard JSON marshaller. As it doesn't marshal unexported fields, I needed to write a custom MarshalJSON function, which worked perfectly. When I called json.Marshal on the parent struct containing the ones that needed custom marshalling as fields, it worked fine.

Now I need to marshal everything to BSON for some MongoDB work, and I can't find any documentation about how to write custom BSON marshalling. Can anyone tell me how to do the equivalent for BSON/mgo for what I've demonstrated below?

currency.go (the important parts)

type Currency struct {
    value        decimal.Decimal //The actual value of the currency.
    currencyCode string          //The ISO currency code.
}

/*
MarshalJSON implements json.Marshaller.
*/
func (c Currency) MarshalJSON() ([]byte, error) {
    f, _ := c.Value().Float64()
    return json.Marshal(struct {
        Value        float64 `json:"value" bson:"value"`
        CurrencyCode string  `json:"currencyCode" bson:"currencyCode"`
    }{
        Value:        f,
        CurrencyCode: c.CurrencyCode(),
    })
}

/*
UnmarshalJSON implements json.Unmarshaller.
*/
func (c *Currency) UnmarshalJSON(b []byte) error {

    decoded := new(struct {
        Value        float64 `json:"value" bson:"value"`
        CurrencyCode string  `json:"currencyCode" bson:"currencyCode"`
    })

    jsonErr := json.Unmarshal(b, decoded)

    if jsonErr == nil {
        c.value = decimal.NewFromFloat(decoded.Value)
        c.currencyCode = decoded.CurrencyCode
        return nil
    } else {
        return jsonErr
    }
}

product.go (again, just the relevant parts)

type Product struct {
    Name  string
    Code  string
    Price currency.Currency
}

When I call json.Marshal(p) where p is a Product, it produces the output I want without the need for the pattern (not sure of the name) where you create a struct which is just a clone with all exported fields.

In my opinion using the inline method I've used greatly simplifies the API and stops you having extra structs that clutter things up.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
leylandski
  • 448
  • 1
  • 4
  • 15
  • I suppose the fact that the Currency struct is declared using unexported fields is a typo ? – SirDarius Jun 17 '15 at 15:45
  • 1
    Nope, it's deliberate. There is more code than what's above and I'm a big proponent of using getters/setters to stop the programmer being able to just change what they want without regard for immutable business logic, plus the layer of abstraction means any change later down the line to the internal workings of my struct means I have to change a minimal amount of code. – leylandski Jun 17 '15 at 15:54
  • And also, if you are a user of the `shopspring/decimal` package, the fields aren't exported – so in either case, you would still have to define a custom Getter/Setter to enable serialisation/deserialisation as shown in the answer. – Benjamin R Aug 11 '16 at 22:39

1 Answers1

23

Custom bson Marshalling/Unmarshalling works nearly the same way, you have to implement the Getter and Setter interfaces respectively

Something like this should work :

type Currency struct {
    value        decimal.Decimal //The actual value of the currency.
    currencyCode string          //The ISO currency code.
}

// GetBSON implements bson.Getter.
func (c Currency) GetBSON() (interface{}, error) {
    f := c.Value().Float64()
    return struct {
        Value        float64 `json:"value" bson:"value"`
        CurrencyCode string  `json:"currencyCode" bson:"currencyCode"`
    }{
        Value:        f,
        CurrencyCode: c.currencyCode,
    }, nil
}

// SetBSON implements bson.Setter.
func (c *Currency) SetBSON(raw bson.Raw) error {

    decoded := new(struct {
        Value        float64 `json:"value" bson:"value"`
        CurrencyCode string  `json:"currencyCode" bson:"currencyCode"`
    })

    bsonErr := raw.Unmarshal(decoded)

    if bsonErr == nil {
        c.value = decimal.NewFromFloat(decoded.Value)
        c.currencyCode = decoded.CurrencyCode
        return nil
    } else {
        return bsonErr
    }
}
HectorJ
  • 5,814
  • 3
  • 34
  • 52
  • 2
    That's exactly what I was looking for! It's kind of annoying that they've not named it in a similar fashion to the JSON one, but what can you do. – leylandski Jun 17 '15 at 13:24
  • 2
    The only change needed to make this a perfect solution is to remove the bson.Marshal statement around the return struct in the get method as described by [this answer](http://stackoverflow.com/questions/30895854/why-wont-mgo-unmarshall-my-struct-properly/30897080#30897080) – leylandski Jun 18 '15 at 08:25
  • @Adam : Indeed, thanks. Edited my answer to reflect it – HectorJ Jun 18 '15 at 11:59
  • 1
    Hi currently I'm using this driver go.mongodb.org/mongo-driver/mongo How can I achieve the same thing as you had. – Rohman HM May 19 '20 at 09:27