I must admit that I tend to overlook the interfaces in the encoding package. Since I typically work with JSON or Yaml, so I tend to care more about methods like UnmarshalJSON. However, today I learned the trick of using TextUnmarshaler when I needed to use a custom type inside the struct.

Problem

Consider this code

package main

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

type Foo struct {
  BaseURL *url.URL `json:"base_url"`
}

func main() {
  const src = `{"base_url": "https://example.com"}`
  var aFoo Foo
  err := json.Unmarshal([]byte(src), &aFoo)
  if err != nil {
    panic(err)
  }
  fmt.Printf("%#+v\n", aFoo.BaseURL.String())
}

The standard *url.URL can’t do that. There’s UnmarshalBinary, but no other method available.

panic: json: cannot unmarshal string into Go struct field Foo.base_url of type url.URL

UnmarshalText

json.Unmarshal documents how it works

To unmarshal JSON into a value implementing [Unmarshaler], Unmarshal calls that value’s [Unmarshaler.UnmarshalJSON] method, including when the input is a JSON null. Otherwise, if the value implements [encoding.TextUnmarshaler] and the input is a JSON quoted string, Unmarshal calls [encoding.TextUnmarshaler.UnmarshalText] with the unquoted form of the string.

The only thing needed to be implemented is TextUnmarshaler interface. Because URL is a string in JSON.

type URL struct {
  *url.URL
}

func (u *URL) UnmarshalText(text []byte) error {
  parsed, err := url.Parse(string(text))
  if err != nil {
    return err
  }
  u.URL = parsed
  return nil
}

And the output is "https://example.com".

Marshal it back

When marshaled back to JSON, the URL looks like this: No one wants to write URLs like this.

{
  "u": {
    "Scheme": "https",
    "Opaque": "",
    "User": null,
    "Host": "example.com",
    "Path": "",
    "RawPath": "",
    "OmitHost": false,
    "ForceQuery": false,
    "RawQuery": "",
    "Fragment": "",
    "RawFragment": ""
  }
}

Obviously there is an interface for that

func (u URL) MarshalText() ([]byte, error) {
  if u.URL == nil {
    return []byte{}, nil
  }
  return []byte(u.URL.String()), nil
}
{"base_url":"https://example.com"}

Conclusion

While the text encoding methods are somewhat less popular than their encoding/json counterparts, they can be still useful.

See complete example on Go playground.

Logo is remixed github.com/egonelbre/gophers and is under CC0 license like an original.