Interfaces for implementing custom marshaling and unmarshaling behavior for user-defined types. Implement these interfaces to control how your types are represented in YAML.
The Marshaler interface may be implemented by types to customize their behavior when being marshaled into a YAML document.
type Marshaler interface {
MarshalYAML() (interface{}, error)
}Returns a value that will be marshaled in place of the original value.
MarshalYAML() (interface{}, error)interface{} - The value to marshal instead of the original typeerror - Error if marshaling fails, nil otherwiseMarshalerCustom timestamp formatting:
package main
import (
"fmt"
"gopkg.in/yaml.v3"
"time"
)
type CustomTime struct {
time.Time
}
func (ct CustomTime) MarshalYAML() (interface{}, error) {
// Return custom formatted string
return ct.Format("2006-01-02 15:04:05"), nil
}
type Event struct {
Name string
Timestamp CustomTime
}
func main() {
event := Event{
Name: "UserLogin",
Timestamp: CustomTime{time.Now()},
}
data, err := yaml.Marshal(&event)
if err != nil {
panic(err)
}
fmt.Printf("%s", data)
// Output:
// name: UserLogin
// timestamp: 2024-01-15 10:30:45
}Return a yaml.Node for fine-grained control:
type CustomMap map[string]string
func (cm CustomMap) MarshalYAML() (interface{}, error) {
// Create a mapping node with flow style
node := &yaml.Node{
Kind: yaml.MappingNode,
Style: yaml.FlowStyle,
}
for key, value := range cm {
node.Content = append(node.Content,
&yaml.Node{Kind: yaml.ScalarNode, Value: key},
&yaml.Node{Kind: yaml.ScalarNode, Value: value},
)
}
return node, nil
}
type Config struct {
Name string
Settings CustomMap
}
func main() {
config := Config{
Name: "MyApp",
Settings: CustomMap{
"debug": "true",
"timeout": "30",
},
}
data, _ := yaml.Marshal(&config)
fmt.Printf("%s", data)
// Output:
// name: MyApp
// settings: {debug: true, timeout: "30"}
}Redact sensitive information during marshaling:
type Password string
func (p Password) MarshalYAML() (interface{}, error) {
if p == "" {
return nil, nil
}
return "********", nil
}
type Database struct {
Host string
Port int
Username string
Password Password
}
func main() {
db := Database{
Host: "localhost",
Port: 5432,
Username: "admin",
Password: "secret123",
}
data, _ := yaml.Marshal(&db)
fmt.Printf("%s", data)
// Output:
// host: localhost
// port: 5432
// username: admin
// password: '********'
}The Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a YAML document.
type Unmarshaler interface {
UnmarshalYAML(value *Node) error
}Custom unmarshaling logic for the type.
UnmarshalYAML(value *Node) errorvalue (*Node) - The YAML node to unmarshal fromerror - Error if unmarshaling fails, nil otherwise*yaml.Node representing the YAML contentFlexible string or object unmarshaling:
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
type Duration struct {
Seconds int
}
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
// Try to unmarshal as integer (seconds)
var seconds int
if err := value.Decode(&seconds); err == nil {
d.Seconds = seconds
return nil
}
// Try to unmarshal as string (e.g., "30s", "5m")
var str string
if err := value.Decode(&str); err != nil {
return err
}
// Parse string format
if len(str) < 2 {
return fmt.Errorf("invalid duration: %s", str)
}
var multiplier int
switch str[len(str)-1] {
case 's':
multiplier = 1
case 'm':
multiplier = 60
case 'h':
multiplier = 3600
default:
return fmt.Errorf("unknown duration unit: %c", str[len(str)-1])
}
var value int
fmt.Sscanf(str[:len(str)-1], "%d", &value)
d.Seconds = value * multiplier
return nil
}
type Config struct {
Timeout Duration
}
func main() {
// Test with integer
data1 := []byte("timeout: 30")
var c1 Config
yaml.Unmarshal(data1, &c1)
fmt.Printf("Timeout: %d seconds\n", c1.Timeout.Seconds) // 30 seconds
// Test with string
data2 := []byte("timeout: 5m")
var c2 Config
yaml.Unmarshal(data2, &c2)
fmt.Printf("Timeout: %d seconds\n", c2.Timeout.Seconds) // 300 seconds
}Validate data during unmarshaling:
type Port int
func (p *Port) UnmarshalYAML(value *yaml.Node) error {
var port int
if err := value.Decode(&port); err != nil {
return err
}
if port < 1 || port > 65535 {
return fmt.Errorf("invalid port number: %d (must be 1-65535)", port)
}
*p = Port(port)
return nil
}
type Server struct {
Host string
Port Port
}
func main() {
data := []byte("host: localhost\nport: 99999")
var server Server
err := yaml.Unmarshal(data, &server)
if err != nil {
fmt.Println(err) // invalid port number: 99999 (must be 1-65535)
}
}Support multiple input formats:
type Color struct {
R, G, B uint8
}
func (c *Color) UnmarshalYAML(value *yaml.Node) error {
// Try hex string format (e.g., "#FF0000")
var hex string
if err := value.Decode(&hex); err == nil {
if len(hex) == 7 && hex[0] == '#' {
fmt.Sscanf(hex, "#%02x%02x%02x", &c.R, &c.G, &c.B)
return nil
}
}
// Try object format (e.g., {r: 255, g: 0, b: 0})
var rgb struct {
R uint8 `yaml:"r"`
G uint8 `yaml:"g"`
B uint8 `yaml:"b"`
}
if err := value.Decode(&rgb); err == nil {
c.R, c.G, c.B = rgb.R, rgb.G, rgb.B
return nil
}
return fmt.Errorf("invalid color format")
}
type Theme struct {
Background Color
Foreground Color
}
func main() {
// Hex format
data1 := []byte(`
background: "#FF0000"
foreground: "#0000FF"
`)
var t1 Theme
yaml.Unmarshal(data1, &t1)
fmt.Printf("BG: RGB(%d,%d,%d)\n", t1.Background.R, t1.Background.G, t1.Background.B)
// Object format
data2 := []byte(`
background:
r: 255
g: 0
b: 0
foreground:
r: 0
g: 0
b: 255
`)
var t2 Theme
yaml.Unmarshal(data2, &t2)
fmt.Printf("BG: RGB(%d,%d,%d)\n", t2.Background.R, t2.Background.G, t2.Background.B)
}The IsZeroer interface is used to determine whether a value should be omitted when marshaling with the omitempty flag.
type IsZeroer interface {
IsZero() bool
}Returns whether the value should be considered zero (empty).
IsZero() boolbool - True if the value should be omitted with omitempty, false otherwiseomitempty tagtime.Time implements this interfaceCustom zero-value detection:
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
type OptionalString struct {
Value string
Set bool // Track whether value was explicitly set
}
func (s OptionalString) IsZero() bool {
return !s.Set
}
func (s *OptionalString) UnmarshalYAML(value *yaml.Node) error {
if err := value.Decode(&s.Value); err != nil {
return err
}
s.Set = true
return nil
}
func (s OptionalString) MarshalYAML() (interface{}, error) {
if !s.Set {
return nil, nil
}
return s.Value, nil
}
type Config struct {
Name string `yaml:"name"`
Description OptionalString `yaml:"description,omitempty"`
Author OptionalString `yaml:"author,omitempty"`
}
func main() {
// With empty string explicitly set
c1 := Config{
Name: "MyApp",
Description: OptionalString{Value: "", Set: true},
}
data1, _ := yaml.Marshal(&c1)
fmt.Printf("%s", data1)
// Output:
// name: MyApp
// description: ""
// With value not set
c2 := Config{
Name: "MyApp",
Description: OptionalString{Value: "", Set: false},
}
data2, _ := yaml.Marshal(&c2)
fmt.Printf("%s", data2)
// Output:
// name: MyApp
}Distinguish between nil pointer and empty value:
type Database struct {
Host string
Port int
}
func (db Database) IsZero() bool {
// Consider zero only if both host and port are unset
return db.Host == "" && db.Port == 0
}
type Config struct {
Database Database `yaml:"database,omitempty"`
}
func main() {
// Empty database - will be omitted
c1 := Config{}
data1, _ := yaml.Marshal(&c1)
fmt.Printf("%s", data1)
// Output: {}
// Database with only port set - will NOT be omitted
c2 := Config{
Database: Database{Port: 5432},
}
data2, _ := yaml.Marshal(&c2)
fmt.Printf("%s", data2)
// Output:
// database:
// host: ""
// port: 5432
}You can implement multiple interfaces together:
type SmartValue struct {
value string
}
func (sv SmartValue) MarshalYAML() (interface{}, error) {
// Custom marshaling
return strings.ToUpper(sv.value), nil
}
func (sv *SmartValue) UnmarshalYAML(value *yaml.Node) error {
// Custom unmarshaling
var str string
if err := value.Decode(&str); err != nil {
return err
}
sv.value = strings.ToLower(str)
return nil
}
func (sv SmartValue) IsZero() bool {
// Custom zero detection
return sv.value == ""
}
type Config struct {
Name SmartValue `yaml:"name,omitempty"`
}
func main() {
// Unmarshal converts to lowercase
data := []byte("name: HELLO")
var c Config
yaml.Unmarshal(data, &c)
fmt.Printf("Internal: %s\n", c.Name.value) // "hello"
// Marshal converts to uppercase
output, _ := yaml.Marshal(&c)
fmt.Printf("%s", output) // "name: HELLO"
}Both MarshalYAML and UnmarshalYAML can return errors:
func (ct CustomType) MarshalYAML() (interface{}, error) {
if !ct.IsValid() {
return nil, fmt.Errorf("cannot marshal invalid CustomType")
}
return ct.value, nil
}
func (ct *CustomType) UnmarshalYAML(value *yaml.Node) error {
var data string
if err := value.Decode(&data); err != nil {
return fmt.Errorf("failed to decode CustomType: %w", err)
}
if !validateData(data) {
return fmt.Errorf("invalid data for CustomType: %s", data)
}
ct.value = data
return nil
}Marshaler, consider implementing Unmarshaler for symmetryfunc (t *Type) UnmarshalYAML to modify the receiver*yaml.Node from MarshalYAMLIsZeroer interface is particularly useful with time.Time and other types where the zero value is meaningfulyaml.Node for maximum control over the output structure