Unmarshal yaml while keeping extra parameters

48 Views Asked by At

I've been trying to write a custom UnmarshalYAML function that will unmarshal a yaml to a struct and also keep unexpected parameters saving them into a map into the struct itself.

I've tried writing this (https://go.dev/play/p/azf2hksriQ1) but, of course, I get an infinite loop when I try to unmarshal the struct inside the UnmarshalYAML.

Does anyone have any suggestion on how to make this work? Thank you very much!

2

There are 2 best solutions below

1
2r2w On BEST ANSWER

You can use a simple wrapping technique where you define a type alias for your original type (so yaml.Unmarshaler won't call your UmarshalYAML method) and create a struct which embeds all known and unknown parameters.

type Foo struct {
    Param1     string         `yaml:"param1"`
    Param2     []int          `yaml:"param2"`
    // NOTE: we explicitly ignore this parameter so it won't be 
    // eventually unmarshaled 
    Extensions map[string]any `yaml:"-"` 
}

// UnmarshalYAML implements the yaml.UnmarshalYAML interface.
func (f *Foo) UnmarshalYAML(value *yaml.Node) error {
    // Here we are wrapping Foo, so it will be unmarshaled
    // using without calling foo.UnmarshalYAML
    type Wrapped Foo
    // NOTE: for yaml it's required to provide inline
    // option if we would like to embed
    var val struct {
        Wrapped    `yaml:",inline"`
        Extensions map[string]any `yaml:",inline"`
    }
    if err := value.Decode(&val); err != nil {
        return err
    }
    *f = Foo(val.Wrapped)
    f.Extensions = val.Extensions
    return nil
}

Playground Link

1
flyx On

Wrap your type with an unmarshaler type, which then calls Decode on the wrapped value:


type Foo struct {
    Param1     string         `yaml:"param1"`
    Param2     []int          `yaml:"param2"`
    Extensions map[string]any `yaml:"extensions"`
}

type FooUnmarshaler struct {
    Foo
}

// UnmarshalYAML implements the yaml.UnmarshalYAML interface.
func (f *FooUnmarshaler) UnmarshalYAML(value *yaml.Node) error {
    // ... snip ...

    // call Decode on the contained value, which will use default decoding
    if err = value.Decode(&f.Foo); err == nil {
        return nil
    }

    // ... snip ...
}

func main() {
    // ... snip ...

    // user your unmarshaler type
    foo := FooUnmarshaler{}

    err := yaml.Unmarshal([]byte(y), &foo)
    if err != nil {
        panic(fmt.Errorf("Error unmarshaling: %v", err))
    }

    // access the actual data via foo.Foo
    spew.Dump(foo.Foo)
}

Playground link, output:

(main.Foo) {
 Param1: (string) (len=4) "test",
 Param2: ([]int) (len=3 cap=3) {
  (int) 2,
  (int) 4,
  (int) 6
 },
 Extensions: (map[string]interface {}) (len=1) {
  (string) (len=5) "x-foo": (string) (len=3) "bar"
 }
}