viper.Unmarshal() ignoring UnmarshalYAML interface

Created on 21 Apr 2017  路  4Comments  路  Source: spf13/viper

I am trying to implement a custom unmarshaler for a YAML config to allow a key to support multiple types (in my personal case, a string and sequence). I am trying to configure that if the YAML value is a string type, I can manipulate it into a sequence (e.g. splitting on spaces, quotes, etc).

package main

import (
    "bytes"
    "fmt"

    "github.com/kr/pretty"
    "github.com/spf13/viper"
)

type CommandType []string

type Config struct {
    Project string
    Alias   map[string]struct {
        Command     CommandType
        Environment string
    }
}

func (cmd *CommandType) UnmarshalYAML(unmarshal func(interface{}) error) error {
    if err := unmarshal(&cmd); err != nil {
        return err
    }
    // Simply overwrite unmarshalled data PoC
    *cmd = []string{"overwritten", "values"}
    return nil
}

var data = []byte(`---
project: proof-of-concept
alias:
  poc:
    command: this can be split
    environment: poc
  other:
    command:
      - this
      - can
      - be
      - left
    environment: poc`)

func main() {
    viper.SetConfigType("yaml")
    viper.ReadConfig(bytes.NewBuffer(data))

    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        panic(fmt.Errorf("Fatal error loading config: %s \n", err))
    }
    fmt.Printf("%# v\n", pretty.Formatter(cfg))
}

With the above code, I expect an output of:

main.Config{
    Project: "proof-of-concept",
    Alias:   {
        "poc": {
            Command:     {"overwritten", "values"},
            Environment: "poc",
        },
        "other": {
            Command:     {"overwritten", "values"},
            Environment: "poc",
        },
    },
}

but I get the unchanged original data instead:

main.Config{
    Project: "proof-of-concept",
    Alias:   {
        "poc": {
            Command:     {"this can be split"},
            Environment: "poc",
        },
        "other": {
            Command:     {"this", "can", "be", "left"},
            Environment: "poc",
        },
    },
}

I'm not 100% sure I have the interface right, but the API doc for gopkg.in/yaml.v2 doesn't go into detail.

Most helpful comment

Took me a while to find the reason...

If you call ReadInConfig() or ReadConfig() on a viper-object, it first parses everything into a map[string]interface{}. However, your structure you want to marshal into is not in play yet, so the used yaml package will blindly parse everything into the map (as your struct types aren't known there), so all UnmarshalYAML (and even UnmarshalJSON) functions will be completely ignored.
If you now unmarshal into your struct, viper uses mapstructure to now map the map[string]interface{} to your structs, but it's not YAML any more.

Ideally mapstructure would provide similar unmarshaling interfaces so this can be handled easily, like

func (m *MyType) UnmarshalMap(value interface{}) error {
    // your code ...
}

Imo this is not a bug in viper itself, that's kind of "by design".

(Filed an issue on mapstructure as well to implement such a marshaling interface: https://github.com/mitchellh/mapstructure/issues/115)

All 4 comments

Just had the same problem and my interface looked the same as yours. @TimJones did you find a way to do this?

@augier Sorry this went so long without a reply. Unfortunately I never found a way around this, I simply had to force one format over the other. If you had any better luck I'd be interested in hearing about it!

Took me a while to find the reason...

If you call ReadInConfig() or ReadConfig() on a viper-object, it first parses everything into a map[string]interface{}. However, your structure you want to marshal into is not in play yet, so the used yaml package will blindly parse everything into the map (as your struct types aren't known there), so all UnmarshalYAML (and even UnmarshalJSON) functions will be completely ignored.
If you now unmarshal into your struct, viper uses mapstructure to now map the map[string]interface{} to your structs, but it's not YAML any more.

Ideally mapstructure would provide similar unmarshaling interfaces so this can be handled easily, like

func (m *MyType) UnmarshalMap(value interface{}) error {
    // your code ...
}

Imo this is not a bug in viper itself, that's kind of "by design".

(Filed an issue on mapstructure as well to implement such a marshaling interface: https://github.com/mitchellh/mapstructure/issues/115)

Great work @tubocurarin !! This was quite the gotcha for me!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lllama picture lllama  路  3Comments

noandrea picture noandrea  路  4Comments

xh3b4sd picture xh3b4sd  路  6Comments

orian picture orian  路  4Comments

jgoodall picture jgoodall  路  5Comments