Libove Blog

Personal Blog about anything - mostly programming, cooking and random thoughts

Go: Custom Date/Time Format in YAML

The yaml package in Go uses a default time format for marshalling and will only accept this format when reading a yaml file. This is ok if you use the serializer to exchange data between programs, but quickly falls apart when user generated files have to be parsed. As I'm using yaml headers in Markdown files (and yaml files in various other places) to generate this blog, I needa more robust and user friendly way to set publishing dates. Ideally the blog software should accept any time format.

For this guide I will assume that we have a yaml format like this:

title: "How to parse YAML dates"
date: 17-10-2022

The corresponding definition in Go looks like this:

type Data struct {
	Title   string    `yaml:"title"`
	Date    time.Time `yaml:"date"`
}

When we try to unmarshal the above yaml it will result in an error: parsing time "17-10-2022" as "2006-01-02T15:04:05Z07:00": cannot parse "0-2022" as "2006". To support this format the unmarshal behavior of the Data struct has to be overwritten.

func (d *Data) UnmarshalYAML(unmarshal func(interface{}) error) error {
	type T struct {
		Title string `yaml:"title"`
	}
	type S struct {
		Date string `yaml:"date"`
	}

	// parse non-time fields
	var t T
	if err := unmarshal(&t); err != nil {
		return err
	}
	d.Title = t.Title

	// parse time field
	var s S

	if err := unmarshal(&s); err != nil {
		return err
	}

	possibleFormats := []string{
		"02-01-2006",
		time.Layout,
		time.ANSIC,
	}
	var found_time bool = false
	for _, format := range possibleFormats {
		if t, err := time.Parse(format, s.Date); err == nil {
			d.Date = t
			found_time = true
			break
		}
	}

	if !found_time {
		return fmt.Errorf("could not parse date: %s", s.Date)
	}

	return nil
}

Let's break this down.

func (d *Data) UnmarshalYAML(unmarshal func(interface{}) error) error {

In order to control the unmarhalling of a yaml file, we have to provide an UnmarshalYAML function for the struct. This function is passed an unmarshal function which can be used to parse arbitrary structs. We will use this function multiple times to retrieve different parts of the yaml file.

	type T struct {
		Title string `yaml:"title"`
	}
	type S struct {
		Date string `yaml:"date"`
	}

Next we separate our Data struct into two parts; one for everything we just want to keep as it is and one for the date field. Note that the date field is using string as its type, as we want to do the time parsing ourselves.

	// parse non-time fields
	var t T
	if err := unmarshal(&t); err != nil {
		return err
	}
	d.Title = t.Title

For all fields that we want to keep untouched, we just have to call the unmarshal function and, if no error occured, copy the data to our data struct. To parse the date we start similarily, by getting the time string.

	// parse time field
	var s S

	if err := unmarshal(&s); err != nil {
		return err
	}

Aftwards the function loops over all formats that we want to support and check if the string matches any format. The list of possible formats can be extended. I kept it short in this example. You can find the full list I use at the bottom of the tutorial.

	possibleFormats := []string{
		"02-01-2006",
		time.Layout,
		time.ANSIC,
	}
	var found_time bool = false
	for _, format := range possibleFormats {
		if t, err := time.Parse(format, s.Date); err == nil {
			d.Date = t
			found_time = true
			break
		}
	}

Finally, we check if any format matched.

	if !found_time {
		return fmt.Errorf("could not parse date: %s", s.Date)
	}

	return nil

Now our Data struct will accept any time format we add to the possibleFormats list. This is the full list I currently use in my blog software, the latest code can be found in the owl-blogs project.

	possibleFormats := []string{
		"2006-01-02",
		time.Layout,
		time.ANSIC,
		time.UnixDate,
		time.RubyDate,
		time.RFC822,
		time.RFC822Z,
		time.RFC850,
		time.RFC1123,
		time.RFC1123Z,
		time.RFC3339,
		time.RFC3339Nano,
		time.Stamp,
		time.StampMilli,
		time.StampMicro,
		time.StampNano,
	}