Play with Marshaler
Leveraging Unmarshaler interface in golang

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:
- The cat's name must not be empty.
- 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 ifCat
directly called its ownUnmarshalJSON
method. - After validation, the
ToCat
method convertsRawCat
into aCat
.
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.