on
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.