Improved Golang JSON unmarshaling with time and URL

The world is mirrored.
Image courtesy of shezamm

Often times we need to unmarshal upstream data with unique constraints. Such as custom data types, or custom parsing of specific formats. Using the standard library could be impractical, and handling long structs manually can be tedious; however there is an alternative.

While it is possible to use the json.UnmarshalJSON interface to create a custom unmarshaler using a temporary struct that mirrors our data type this can become complicated when the data type has many fields, dozens or hundreds. It becomes laborious to create a temp value with hundreds of fields. It’s annoying to create and clutters the source code; however there another option.

We can use an embedded clone type of our data and only handle the fields that require our attention.

For our example we will use this Person struct. We want to manually handle the time and URL. These fields could be any that the standard Json library can not handle itself, or that require manual handling; such as parsing a custom time format from a legacy system.

1
2
3
4
5
6
7
type Person struct {
	ID       string    `json:"id"`
	Name     string    `json:"name"`
	DOB      time.Time `json:"dob"`
	Homepage url.URL   `json:"homepage"`
  // pretend there are a thousand other fields.
}

The first thing we are going to do in our custom Unmarshaling function is declare a new type that is identical in layout as our Person data type.

After we declare our type, we will create a temporary struct with the fields we have to handle manually, and our type embedded as a pointer.

We set our method receiver, and convert it to our clone type. We can do this because they are identical in layout and underlying type. We must ensure that we convert the method receiver as a pointer type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (p *Person) UnmarshalJSON(data []byte) error {
	type Doppelganger Person

	tmp := struct {
		DOB      string `json:"dob"`
		Homepage string `json:"homepage"`
		*Doppelganger
	}{
		Doppelganger: (*Doppelganger)(p),
	}

Now we will unmarshal as normal into our temporary variable. Because our embedded type is a pointer all unmarshaled values, except those we are handling specifically, are assigned to the fields of our method receiver; that is to our original data structure.

1
2
3
4
	err := json.Unmarshal(data, &tmp)
	if err != nil {
		return err
	}

Next we handling our custom fields manually in any way that is required. If we needed to validate a UUID, or some esoteric serialized format, we would do it here. We assign the final value to the correct field of the method receiver.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
	p.DOB, err = time.Parse(time.RFC3339, tmp.DOB)
	if err != nil {
		return err
	}

	homepage, err := url.Parse(tmp.Homepage)
	if err != nil {
		return err
	}
	p.Homepage = *homepage

And lastly we return without error.

1
2
	return nil
}

This little bit of code requires some smoke and mirrors to get working, but once you have used it you will find yourself using even for small types that require custom handling.

Here is the full example with all of the previous code blocks together. You can also view a working example on the Go Playground.

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
	"encoding/json"
	"fmt"
	"net/url"
	"time"
)

func main() {
	var p Person

	err := json.Unmarshal(data, &p)
	if err != nil {
		fmt.Println("error: ", err)
		return
	}

	fmt.Printf("Person: %+v \n", p)
}

type Person struct {
	ID       string    `json:"id"`
	Name     string    `json:"name"`
	DOB      time.Time `json:"dob"`
	Homepage url.URL   `json:"homepage"`
  // pretend there are a thousand other fields.
}

func (p *Person) UnmarshalJSON(data []byte) error {
	type Doppelganger Person

	tmp := struct {
		DOB      string `json:"dob"`
		Homepage string `json:"homepage"`
		*Doppelganger
	}{
		Doppelganger: (*Doppelganger)(p),
	}

	err := json.Unmarshal(data, &tmp)
	if err != nil {
		return err
	}

	p.DOB, err = time.Parse(time.RFC3339, tmp.DOB)
	if err != nil {
		return err
	}

	homepage, err := url.Parse(tmp.Homepage)
	if err != nil {
		return err
	}
	p.Homepage = *homepage

	return nil
}

var data = []byte(`{
"id": "unique-id",
"name": "Alice Jones",
"dob": "1977-10-23T13:30:05-05:00",
"homepage": "https://example.com/alice"
}`)
Improved Golang JSON unmarshaling with time and URL by
  tools  golang 
Like what you read? Share it:
  Facebook   Email