Go’s standard library has a convenient way of handling JSON verification and default values through the Marshaler and Unmarshaler interfaces. This means we don’t necessarily need separate methods to handle this data verification/manipulation.

Unmarshaler

Let’s pretend our system can’t allow users that are under the age 13 and we need a default timezone.

var (
	ErrTooYoung   = fmt.Errorf("too young")
	ErrTZNotFound = fmt.Errorf("invalid timezone, must be one of %v", timezones)
)

type TimeZone string

var timezones = [...]TimeZone{"UTC", "America/New_York"}

type User struct {
	Name string   `json:"name"`
	Age  int      `json:"age"`
	TZ   TimeZone `json:"timezone"`
}

func (t TimeZone) Validate() error {
	for _, v := range timezones {
		if v == t {
			return nil
		}
	}

	return ErrTZNotFound
}

func (u *User) UnmarshalJSON(b []byte) error {
	type UserAlias User
	ua := &struct {
		*UserAlias
	}{
		UserAlias: (*UserAlias)(u),
	}

	if err := json.Unmarshal(b, ua); err != nil {
		return err
	}

	if u.Age <= 13 {
		return ErrTooYoung
	}

	if u.TZ == "" {
		u.TZ = "UTC"
	}

	if err := u.TZ.Validate(); err != nil {
		return err
	}

	return nil

}

You might be wondering why we declare an inline struct called UserAlias. The problem is if we don’t declare this new type and you try to unmarshal the data into u it will create a recursive never ending call to the unmashal method. Instead, we create the inline struct that will unmarshal only to our local type, but will unmarshal into u the way we expect. Once we unmarshal our data, we then run any verifications/defaults against our struct. In this case, we just check if the user age is 13 or less and check the timezone value.

You could do this in solely in separate validation methods/functions but that means you would need to call them manually every time you want to unmarshal this type.

Marshaler

The marshaler is similar. One catch with the marshaler however, is that to just call json.Marshal(userStruct) the method must take User as the copy value instead of a pointer. If you want to use a pointer you will need to call json.Marshal(&userStruct). Here we apply the same logic to marshaling the data as we did above to unmarshaling the data. You also need to make sure you marshal the local type instead of the actual type because you will run into the same infinite recursion as with the unmarshaler.

func (u User) MarshalJSON() ([]byte, error) {
	type UserAlias User
	ua := &struct {
		UserAlias
	}{
		UserAlias: (UserAlias)(u),
	}

	if ua.Age <= 13 {
		return nil, ErrTooYoung
	}

	if ua.TZ == "" {
		u.TZ = "UTC"
	}

	if err := ua.TZ.Validate(); err != nil {
		return nil, ErrTZNotFound
	}

	return json.Marshal(ua)
}

Complete Code

The final code (with the validations in their own method) would look like this:

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
)

var (
	ErrTooYoung   = fmt.Errorf("too young")
	ErrTZNotFound = fmt.Errorf("invalid timezone, must be one of %v", timezones)
)

type TimeZone string

var timezones = [...]TimeZone{"UTC", "America/New_York"}

type User struct {
	Name string   `json:"name"`
	Age  int      `json:"age"`
	TZ   TimeZone `json:"timezone"`
}

func (t TimeZone) Validate() error {
	for _, v := range timezones {
		if v == t {
			return nil
		}
	}

	return ErrTZNotFound
}

func (u *User) Validate() error {
	if u.Age <= 13 {
		return ErrTooYoung
	}

	return u.TZ.Validate()
}

func (u *User) UnmarshalJSON(b []byte) error {
	type UserAlias User
	ua := &struct {
		*UserAlias
	}{
		UserAlias: (*UserAlias)(u),
	}

	if err := json.Unmarshal(b, ua); err != nil {
		return fmt.Errorf("%w", err)
	}

	if u.TZ == "" {
		u.TZ = "UTC"
	}

	return u.Validate()

}

func (u User) MarshalJSON() ([]byte, error) {
	type UserAlias User
	ua := &struct {
		UserAlias
	}{
		UserAlias: (UserAlias)(u),
	}

	if u.TZ == "" {
		u.TZ = "UTC"
	}

	if err := u.Validate(); err != nil {
		return nil, fmt.Errorf("%w", err)
	}

	return json.Marshal(ua)
}

func main() {
	data := `{"name": "John", "age": 12, "timezone": "UTC"}`

	var user User

	if err := json.Unmarshal([]byte(data), &user); err != nil {
		log.Fatal(err) // should error on user being too young
	}

	u := User{
		Name: "John",
		Age:  14,
		TZ:   "utc", // should error on incorrect timezone
	}

	b, err := json.Marshal(u)
	if err != nil {
		log.Fatal(errors.Unwrap(err))
	}

	fmt.Printf("%#v", user)
	fmt.Println(string(b))
}

Update: Added error unwrapping when marshaling to get just the relevant error.