It is possible to significantly change the marshalization of a structure in json only through the MarshalJSON () method, having written the complete implementation of the marshalling there. How exactly? To this documentation, Go does not provide any answers or recommendations, providing, so to speak, complete freedom. And how to use this freedom so as not to pile up a bunch of crutches, transferring all the logic to MarshalJSON (), and then not sawing this poor constantly expanding function with the next custom json?
The solution is really simple:
(The second point will not be, the first will suffice.)
It is this approach that will save you from a ton of shitty code, tangled code, a lot of rework and a certain kind of fun before an important release. Let's look not at an example in the documentation, where json is customized for a simple int, and the whole logic of the model for several lines, but on our original task.
Do we really need to change the structure of our object and cram a bunch of crutches? Did the severity of the language, which provides for the one-to-one correspondence of json attributes and the structure itself, suddenly interfere with us?
The initial task is to get such JSON structures of some approved formats. In the initial problem about crutches nothing is said. It is said about different data structures. And we use the same data type (struct) to store this data. Thus, our single entity must have several representations. So we got the correct interpretation of the problem.
We need to make several views for our data type. Do not change the conversion to json for a specific case, but in principle have several views, one of which is the default view.
So, we have another entity - representation .
And let's get to the examples and, actually, to the code.
Suppose we have a bookstore that sells books. Everything we have built on microservices, and one of them gives the data on requests in json format. Books were first unloaded only on the site window. Then we connect to various partner networks, and, for example, provide university students with books at a special price. And recently, our marketers suddenly decided to hold some kind of promotions and they also need their own price and some other text. Let some other microservice deal with calculating prices and preparing texts, which adds already prepared data to the database.
So, the evolution of our model of the book has reached such a disgrace:
type Book struct { Id int64 Title string Description string Partner2Title string Price int64 PromoPrice int64 PromoDescription string Partner1Price int64 Partner2Price int64 UpdatedAt time.Time CreatedAt time.Time view BookView }
The last attribute (view) is non-exportable (private), it is not part of the data, but is the storage location for the very same view , which contains the information in which json to collapse to the object. In the simplest case, this is just interface {}
type BookView interface{}
We can also add to the interface of our view any method, for example Prepare (), which will be called in MarshalJSON () and somehow prepare, validate, or log the output structure.
Now let's describe our views and the function itself.
type SiteBookView struct { Id int64 `json:"sku"` Title string `json:"title"` Description string `json:"description"` Price int64 `json:"price"` } type Partner1BookView struct { Id int64 `json:"bid"` Title string `json:"title"` Partner1Price int64 `json:"price"` } type Partner2BookView struct { Id int64 `json:"id"` Partner2Title string `json:"title"` Description string `json:"description"` Partner2Price int64 `json:"price"` } type PromoBookView struct { Id int64 `json:"ref"` Title string `json:"title"` Description string `json:"description"` PromoPrice int64 `json:"price"` PromoDescription string `json:"promo,omitempty"` } func (b Book) MarshalJSON() (data []byte, err error) { // , if b.view == nil { // , b.SetDefaultView() } // var buff bytes.Buffer // , enc := gob.NewEncoder(&buff) // , , dec := gob.NewDecoder(&buff) // err = enc.Encode(b) if err != nil { return } // err = dec.Decode(b.view) if err != nil { return } // return json.Marshal(b.view) }
Sending and receiving data between structures occurs according to the principle of matching attribute names, and the types do not have to be exactly the same, for example, they can be sent from int64, but received in int, but not in uint.
The final step is to marshal the installed view with the data, using all the power of the standard description via json tags (`json:"promo,omitempty"`)
A very important requirement for the application of this approach is the mandatory registration of model structures and maps. In order to ensure that all structures are always registered, we will add them to the init () function.
func init() { gob.Register(Book{}) gob.Register(SiteBookView{}) gob.Register(Partner1BookView{}) gob.Register(Partner2BookView{}) gob.Register(PromoBookView{}) }
Full model code:
import ( "bytes" "encoding/gob" "encoding/json" "time" ) func init() { gob.Register(Book{}) gob.Register(SiteBookView{}) gob.Register(Partner1BookView{}) gob.Register(Partner2BookView{}) gob.Register(PromoBookView{}) } type BookView interface{} type Book struct { Id int64 Title string Description string Partner2Title string Price int64 PromoPrice int64 PromoDescription string Partner1Price int64 Partner2Price int64 UpdatedAt time.Time CreatedAt time.Time view BookView } type SiteBookView struct { Id int64 `json:"sku"` Title string `json:"title"` Description string `json:"description"` Price int64 `json:"price"` } type Partner1BookView struct { Id int64 `json:"bid"` Title string `json:"title"` Partner1Price int64 `json:"price"` } type Partner2BookView struct { Id int64 `json:"id"` Partner2Title string `json:"title"` Description string `json:"description"` Partner2Price int64 `json:"price"` } type PromoBookView struct { Id int64 `json:"ref"` Title string `json:"title"` Description string `json:"description"` PromoPrice int64 `json:"price"` PromoDescription string `json:"promo,omitempty"` } func (b *Book) SetDefaultView() { b.SetSiteView() } func (b *Book) SetSiteView() { b.view = &SiteBookView{} } func (b *Book) SetPartner1View() { b.view = &Partner1BookView{} } func (b *Book) SetPartner2View() { b.view = &Partner2BookView{} } func (b *Book) SetPromoView() { b.view = &PromoBookView{} } func (b Book) MarshalJSON() (data []byte, err error) { if b.view == nil { b.SetDefaultView() } var buff bytes.Buffer enc := gob.NewEncoder(&buff) dec := gob.NewDecoder(&buff) err = enc.Encode(b) if err != nil { return } err = dec.Decode(b.view) if err != nil { return } return json.Marshal(b.view) }
In the controller there will be something like this:
func GetBooksForPartner2(ctx *gin.Context) { books := LoadBooksForPartner2() for i := range books { books[i].SetPartner2View() } ctx.JSON(http.StatusOK, books) }
Now for the “one more” json change, it is enough just to add another view and not forget to register it in init ().
Source: https://habr.com/ru/post/449090/
All Articles