custom yaml marshaling in go

In Go, you need to have field tags in your structs to instruct YAML machinery to properly marshal and unmarshal, i.e. convert from and to YAML bytes.

type User struct {
  Name    string `yaml:"name"`
  Surname string `yaml:"surname"`
}

This requirement cannot always be easily achievable when you don’t control all the implementation of the types of the fields.

Go playground link.

package main

import (
	"log"
	"net/url"
	
	"gopkg.in/yaml.v3"
)

func main()  {
	type User struct {
		Name    string   `yaml:"name"`
		Surname string   `yaml:"surname"`
		Website *url.URL `yaml:"website"`
	}
	input := `
name: John
surname: Doe
website: https://example.com
`
	var user User
	if err := yaml.Unmarshal([]byte(input), &user); err != nil {
		log.Fatalf("failed to unmarshal: %v", err)
	}
	log.Printf("unmarshaled: %#v", user)
}
failed to unmarshal: yaml: unmarshal errors:
  line 4: cannot unmarshal !!str `https:/...` into url.URL

url.URL has lots of fields like Host, Scheme, RawPath etc. but they don’t have YAML tags and you’d not want to ask for 10 fields just to get a parsed URL. You can convert it to string and then process whenever you read it but that’d increase the cognitive overhead of working with that struct because you added a requirement to all consumers of this struct to inspect and figure out that they need to do additional processing in their code.

Ideally, you’d like to tell the YAML library how to process it once and consumers don’t need to think about it at all. The following code excerpt shows how to do it using gopkg.in/yaml.v3 - v2 of this library has a different interface for these functions.

Go playground link.

package main

import (
	"log"
	"net/url"

	"gopkg.in/yaml.v3"
)
type User struct {
	Name    string   `yaml:"name"`
	Surname string   `yaml:"surname"`
	Website *url.URL `yaml:"-"`
}

func (u *User) UnmarshalYAML(node *yaml.Node) error {
	type original User
	raw := struct {
		original   `yaml:",inline"`
		Website string `yaml:"website"`
	}{}

	if err := node.Decode(&raw); err != nil {
		return err
	}

	if raw.Website != "" {
		parsedURL, err := url.Parse(raw.Website)
		if err != nil {
			return err
		}
		u.Website = parsedURL
	}
	u.Name = raw.Name
	u.Surname = raw.Surname
	return nil
}

func (u *User) MarshalYAML() (interface{}, error) {
	type original User
	raw := struct {
		original   `yaml:",inline"`
		Website string `yaml:"website"`
	}{
		original: original(*u),
	}
	if u.Website != nil {
		raw.Website = u.Website.String()
	}
	return raw, nil
}

func main()  {
	input := `
name: John
surname: Doe
website: https://example.com
`
	var user User
	if err := yaml.Unmarshal([]byte(input), &user); err != nil {
		log.Fatalf("failed to unmarshal: %v", err)
	}
	log.Printf("unmarshaled: %#v", user)
	log.Printf("website url in unmarshaled: %s", user.Website.String())

	output, err := yaml.Marshal(&user)
	if err != nil {
		log.Fatalf("failed to marshal: %v", err)
	}
	log.Printf("marshaled:\n%s", output)
}
2024/05/30 11:05:41 unmarshaled: main.User{Name:"John", Surname:"Doe", Website:(*url.URL)(0x14000140090)}
2024/05/30 11:05:41 website url in unmarshaled: https://example.com
2024/05/30 11:05:41 marshaled:
name: John
surname: Doe
website: https://example.com
Follow @muvaffakonus on Twitter.