December 6, 2024

Play with Marshaler

Leveraging Unmarshaler interface in golang

Play with Marshaler

Sometimes, we need to marshal or unmarshal JSON/YAML inputs and validate them, especially when handling data from an HTTP request. Writing validators for each field can quickly become tedious.
Luckily, Go's encoding/json package provides interfaces that we can implement to bake validation directly into the (un)marshaling process.

I learned that we can leverage the interface in "encoding/json" and implement our own Unmarshaler to include basic validation.

Learn from Source Code

type Unmarshaler interface {
	UnmarshalJSON([]byte) error
}

type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

encoding/json source code

When json.Unmarshal is called, the decoder uses reflection to retrieve the value of each field in the struct. If the type implements the Unmarshaler interface, it invokes the method UnmarshalJSON for addtional processing.
(Same logic applies to the Marshaler, but we focus on Unmarshaler in this article. )

With this in mind, we can implement UnmarshalJSON for our struct to include custom validation logic during the unmarshaling process.

Designing an Unmarshaler

Let’s say we have the following struct representing a cat, and we want to validate two rules:

  1. The cat's name must not be empty.
  2. The cat must be able to meow.

Here’s the initial struct definition:

type RawCat struct {
  Name    string  `json:"name"`
  Age     int     `json:"age"`
  Weight  float64 `json:"weight"`
  CanMeow bool    `json:"can_meow"` 
}

To handle validation, we’ll introduce a separate struct for unmarshaling. After validation, this struct will be converted into the main struct we use in the application. (play with the code)

// The raw input definition
type RawCat struct {
	Name    string  `json:"name"`
	Age     int     `json:"age"`
	Weight  float64 `json:"weight"`
	CanMeow bool    `json:"can_meow"`
}

// Converts validated data into the main struct
func (r *RawCat) ToCat() Cat {
	return Cat{
		Name:    r.Name,
		Age:     r.Age,
		Weight:  r.Weight,
		CanMeow: r.CanMeow,
	}
}

// The main struct used in the application
type Cat struct {
	Name    string
	Age     int
	Weight  float64
	CanMeow bool
}

// UnmarshalJSON with validation
func (c *Cat) UnmarshalJSON(data []byte) error {
	var cat RawCat
	if err := json.Unmarshal(data, &cat); err != nil {
		return err
	}
	if cat.Name == "" {
		return errors.New("name is empty")
	}
	if !cat.CanMeow {
		return errors.New("this cat doesn't meow")
	}
	*c = cat.ToCat()
	return nil
}

Key Notes:

  • The RawCat struct is used only for unmarshaling and validation. It ensures the logic doesn’t recurse into itself, which could happen if Cat directly called its own UnmarshalJSON method.
  • After validation, the ToCat method converts RawCat into a Cat.

Here's the example of both marhshal and unmarshal:
https://go.dev/play/p/eyQQZjfjRcy

Caveats

While this approach bakes validation directly into the (un)marshaling process and keeps our code concise, there is a caveat for the performance.
If we have multiple custom marshalers for deeply nested structures, it can introduce performance issues during (un)marshaling.