go-yaml/yaml

Parsing array of string or single string

ldesplat opened this issue ยท 12 comments

I need to parse some files and some fields can either include an array of strings or can just include 1 element that's inlined. How do I parse such a file?

field: value

or

field:
  - value1
  - value2

I tried field []string but that of course only works for the bottom example and field string only works for the top one. I thought maybe field []string ",inline" but that's limited to structs. Any idea on how to achieve it? One, could think of the inlined string as an array of 1 length...

I have the same problem. Did you find a solution meanwhile?

I managed to get something working using the Unmarshall interface

package main

import (
    "fmt"
    "gopkg.in/yaml.v2"
)

const (
    data = `
attrs:
  foo: bar
  bar:
   - 1
   - 2
   - 3
`
)

type SingleOrMulti struct {
    Values []string
}

func (sm *SingleOrMulti) UnmarshalYAML(unmarshal func(interface{}) error) error {
    var multi []string
    err := unmarshal(&multi)
    if err != nil {
        var single string
        err := unmarshal(&single)
        if err != nil {
            return err
        }
        sm.Values = make([]string, 1)
        sm.Values[0] = single
    } else {
        sm.Values = multi
    }
    return nil
}

type Data struct {
    Attrs map[string]SingleOrMulti
}

func main() {
    var t Data
    yaml.Unmarshal([]byte(data), &t)
    fmt.Printf("%d\n", len(t.Attrs))
    for k, e := range t.Attrs {
        fmt.Printf("%v: %v\n", k, e.Values)
    }
}

Using this I always have a slice in the unmarshaled struct, which gets a single element in case the YAML was a single value.

Note that in this example, attrs: had arbitrary key names so I used a map. You can as well use a struct, if the names are fixed:

type Data struct {
    Foo SingleOrMulti
    Bar SingleOrMulti
}

@dmacvicar I had left this as a todo for later on so thank you very much for finding a solution. You have just taught me how to use this library even more effectively. I did not realize there was an Unmarshaler type! I can now parse some other more complex values right from the beginning. Very awesome.

You closed the issue, but I still think this is an issue. It would be great if one could do something like:

type Data struct {
   Field []string `yaml:"alwaysarray"`
}

And no matter if someone writes:

field: foobar

That would put the value as the only element of the array.

@dmacvicar A slightly modified version of the code above gives you something similar to what you're asking for.

type StringArray []string

func (a *StringArray) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var multi []string
	err := unmarshal(&multi)
	if err != nil {
		var single string
		err := unmarshal(&single)
		if err != nil {
			return err
		}
		*a = []string{single}
	} else {
		*a = multi
	}
	return nil
}

type Data struct {
   Field StringArray
}

Now you can access Data.Field as a string array no matter what.

What's the status? It would be awesome to land this feature.

Any update on this? It should be very good to have this feature.

Building off of dolfelt's code, here's how you could workaround this if you're using v3 of this package:

type StringArray []string

func (a *StringArray) UnmarshalYAML(value *yaml.Node) error {
	var multi []string
	err := value.Decode(&multi)
	if err != nil {
		var single string
		err := value.Decode(&single)
		if err != nil {
			return err
		}
		*a = []string{single}
	} else {
		*a = multi
	}
	return nil
}

type Data struct {
   Field StringArray
}

I ran into this issue recently and have added a patch in a fork here. I'll work to get a full patch submitted as a PR on this repo.

PR added #974. Please let me know if there's anything missing and/or necessary. Happy to update where necessary.

metux commented

Can we move that ticket to some FAQ ?
Right now it looks like an open issue in the ticket list, but actually it's an answered common question.