Imagine you have a struct with some fields which are always there, but you also have some fields which are dynamically populated with different [key, value] pairs, which cannot be described with static types upfront.

Example Record

1
2
3
4
5
6
7
type Order struct {
    ID         string                 `json:"id"`
	ItemID     string                 `json:"item_id"`
    Category   string                 `json:"category"`
    Timestamp  int64                  `json:"timestamp"`
    Attributes map[string]interface{} `json:"attributes"`
}

The attributes field is map and may contain an arbitrary number of values for each record. This is the field that I want to store in JSON column. The other fields are stored in statically defined columns with their respective types.

First attempt

BigQuery infer schema does not work with maps. So doing something like this won’t work.

1
schema, err := bigquery.InferSchema(Order{})

Even if you make the Attributes field type json.RawMessage it won’t work again.

Second attempt

Manually create the schema and try again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
schema := bigquery.Schema{
    {Name: "id", Type: bigquery.StringFieldType},
    {Name: "item_id", Type: bigquery.StringFieldType},
    {Name: "category", Type: bigquery.StringFieldType},
    {Name: "timestamp", Type: bigquery.IntegerFieldType},
    {Name: "attributes", Type: bigquery.JSONFieldType},
}

// ... create the table

err := table.Inserter().Put(ctx, order)

This won’t work (with the Golang client) as BigQuery won’t convert the map to JSON. If we change the field type to json.RawMessage it won’t work again.

Third attempt is OK

We make the Order type implement the bigquery.ValueSaver interface and serialize the attributes field as JSON string.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type OrderValueSaver struct {
    order *Order
}

// Save implements the bigquery.ValueSaver interface 
// to serialize the "attributes" field as JSON string.
func (s *OrderValueSaver) Save() (row map[string]bigquery.Value, insertID string, err error) {
    attr, err := json.Marshal(s.order.Attributes)
    if err != nil {
        return nil, "", err
    }

    row = map[string]bigquery.Value{
        "id":         s.order.ID,
        "item_id":    s.order.ItemID,
        "category":   s.order.Category,
		"timestamp":  s.order.Timestamp,
        "attributes": string(attr),
    }

    return row, s.order.ID, nil
}

func (c *Client) SaveOrder(ctx context.Context, order *Order) error {
    schema := bigquery.Schema{
        {Name: "id", Type: bigquery.StringFieldType},
        {Name: "item_id", Type: bigquery.StringFieldType},
        {Name: "category", Type: bigquery.StringFieldType},
        {Name: "timestamp", Type: bigquery.IntegerFieldType},
        {Name: "attributes", Type: bigquery.JSONFieldType},
    }
	
	// ... create/get table, ideally do it in the constructor of the client once 
	
	saver := &OrderValueSaver{order: order}
    
	return table.Inserter().Put(ctx, saver)
}