Custom JSON marshal/unmarshal

2 min read Tweet this post

Using Go we can have some freedom to use custom data type and customize the way of data will be encoded or decoded. Today we will learn about reading custom data with unique behavior.

I have json string like below, the requirement is reading price number as integer even though in json representative stored as string. How to solve this?

{ "price": "6550", "code": "ASII" }

In simple ways we have 2 solution:

  1. Using intermediate struct to marshal/unmarshal that act as data transfer object then transform to struct with price type int64.
// marshal/unmarshal json using DtoPrice
type DtoTransaction struct {
	Price string `json:"price"`
	Code  string `json:"code"`
}

// then convert to this struct
type Transaction struct {
	Price int64
	Code  string
}
  1. Custom JSON marshal/unmarshal
  • Create custom type StrInt64 with type alias int64
  • Implement MarshalJSON and UnmarshalJSON to manage read string as int64 and vise versa.

Let’s digging more in the code.

<CH.Section>

package customtype

import (
	"encoding/json"
	"fmt"
	"strconv"
)

// StrInt64 is a custom type to easily parse integers in string format
type StrInt64 int64

// MarshalJSON encodes the value into string
func (i StrInt64) MarshalJSON() ([]byte, error) {
	return json.Marshal(fmt.Sprintf("%d", i))
}

// UnmarshalJSON parse encoded string data into int64
func (i *StrInt64) UnmarshalJSON(data []byte) (err error) {
	var str string
	if err := json.Unmarshal(data, &str); err != nil {
		return err
	}
	x, err := strconv.ParseInt(str, 10, 64)
	if err != nil {
		return err
	}

	*i = StrInt64(x)

	return nil
}
package customtype

import (
	"reflect"
	"testing"
)

func TestStrInt64_MarshalJSON(t *testing.T) {
	tests := []struct {
		name    string
		i       StrInt64
		want    []byte
		wantErr bool
	}{
		{
			name:    "1234",
			i:       1234,
			want:    []byte(`"1234"`),
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.i.MarshalJSON()
			if (err != nil) != tt.wantErr {
				t.Errorf("StrInt64.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("StrInt64.MarshalJSON() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestStrInt64_UnmarshalJSON(t *testing.T) {
	type args struct {
		data []byte
	}
	tests := []struct {
		name    string
		i       StrInt64
		args    args
		wantErr bool
	}{
		{
			name:    "1234",
			i:       StrInt64(1234),
			args:    args{data: []byte(`"1234"`)},
			wantErr: false,
		},
		{
			name:    "satu",
			i:       StrInt64(0),
			args:    args{data: []byte(`"satu"`)},
			wantErr: true,
		},
		{
			name:    "is int64",
			i:       StrInt64(0),
			args:    args{data: []byte(`12781`)},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := tt.i.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
				t.Errorf("StrInt64.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

</CH.Section>

Prove it yourself if the implementation above is correct at https://go.dev/play/p/cnx_KdH3e2n

package main

import (
	"encoding/json"
	"fmt"
	"strconv"
)

// StrInt64 is a custom type to easily parse integers in string format
type StrInt64 int64

// MarshalJSON encodes the value into string
func (i StrInt64) MarshalJSON() ([]byte, error) {
	return json.Marshal(fmt.Sprintf("%d", i))
}

// UnmarshalJSON parse encoded string data into int64
func (i *StrInt64) UnmarshalJSON(data []byte) (err error) {
	var str string
	if err := json.Unmarshal(data, &str); err != nil {
		return err
	}
	x, err := strconv.ParseInt(str, 10, 64)
	if err != nil {
		return err
	}

	*i = StrInt64(x)

	return nil
}

type Transaction struct {
	Price StrInt64 `json:"price"`
	Code  string   `json:"code"`
}

func main() {
	var t Transaction
	json.Unmarshal([]byte(`{"price":"6550","code":"ASII"}`), &t)
	t.Price = t.Price * 2
	fmt.Println(t)
	s, _ := json.Marshal(t)
	fmt.Println(string(s))
}

// Output
// {13100 ASII}
// {"price":"13100","code":"ASII"}
go practical json