Dynamically change JSON schema in Go with gob

Significantly change the marshalization of the structure in json is possible only through the MarshalJSON () method, writing there the full implementation of marshalization. How exactly? Go documentation does not give any answers or recommendations to this, providing, so to speak, complete freedom. And how to take advantage of this freedom so as not to pile up a bunch of crutches, transferring all the logic to MarshalJSON (), and then not overwrite this poor constantly growing function with the next json customizations?


The solution is actually simple:


  1. Be honest (honest).

(The second point will not be, the first is enough.)


It is this approach that will save a ton of govnokoda confusing code, a lot of alterations and a certain kind of fun before an important release. Let's look not at the example in the documentation, where json is customized for a simple int, and the whole model logic on several lines, but at our original task.


Do we really need to change the structure of our facility and cram a bunch of crutches? Is it really that the language rigor, which provides for a one-to-one correspondence between the json attributes and the structure itself, suddenly began to interfere with us?


The initial task is to get such JSON structures of some approved formats. In the original problem nothing is said about crutches. 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 representations 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 is built on microservices, and one of them gives data on requests in json format. Books were first uploaded only to the storefront. Then we connect to various affiliate networks, and, for example, provide books for university students at a special price. And recently, our marketers suddenly decided to hold some kind of promotion actions and they also need their own price and some other text of their own. Let some other microservice be involved in the calculation of prices and the preparation of texts, which adds ready-made data to a database.


So, the evolution of our book model 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-exported (private), it is not a part of the data, but is the storage location of the very view , which contains information in which json the object will be folded. In the simplest case, this is just interface {}


type BookView interface{}

We can also add some method to the interface of our view, 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 the names of the attributes, while the types do not have to match exactly, for example, you can send from int64, but accept it in int, but not in uint.


The last step is to marshal the installed data view using the full power of the standard description via json tags (`json:"promo,omitempty"`)


A very important requirement for applying this approach is the mandatory registration of model structures and mappings. To ensure that all structures are always guaranteed to be registered, 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:


Hidden text
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)
}


The controller will have something like this code:


func GetBooksForPartner2(ctx *gin.Context) {
    books := LoadBooksForPartner2()
    for i := range books {
        books[i].SetPartner2View()
    }
    ctx.JSON(http.StatusOK, books)
}

Now for a “one more” json change, just add another view and remember to register it in init ().


Also popular now: