Home > Software design >  Why doesn't YAML.v3 marshal a struct based on the String() in golang?
Why doesn't YAML.v3 marshal a struct based on the String() in golang?

Time:09-28

I have a struct which contains a type based on an enum. I am trying to render it to a user friendly string. Here's minimum viable code:

package main

import (
    "fmt"

    "gopkg.in/yaml.v3"
)

type Job struct {
    Engine Engine `json:"Engine" yaml:"Engine"`
}

//go:generate stringer -type=Engine --trimprefix=Engine
type Engine int

const (
    engineUnknown Engine = iota // must be first
    EngineDocker
    engineDone // must be last
)

func main() {
    j := Job{Engine: EngineDocker}

    fmt.Printf("% v\n\n", j)
    out, _ := yaml.Marshal(j)
    fmt.Println(string(out))
}

Here's the generated code:

// Code generated by "stringer -type=Engine --trimprefix=Engine"; DO NOT EDIT.

package main

import "strconv"

func _() {
    // An "invalid array index" compiler error signifies that the constant values have changed.
    // Re-run the stringer command to generate them again.
    var x [1]struct{}
    _ = x[engineUnknown-0]
    _ = x[EngineDocker-1]
    _ = x[engineDone-2]
}

const _Engine_name = "engineUnknownDockerengineDone"

var _Engine_index = [...]uint8{0, 13, 19, 29}

func (i Engine) String() string {
    if i < 0 || i >= Engine(len(_Engine_index)-1) {
        return "Engine("   strconv.FormatInt(int64(i), 10)   ")"
    }
    return _Engine_name[_Engine_index[i]:_Engine_index[i 1]]
}

Here's the output:

{Engine:1}

Engine: 1

Here's what I'd like the output to be:

{Engine:Docker}

Engine: Docker

I thought the String() in the generated file would accomplish this. Is there any way to do this? Thanks!

CodePudding user response:

yaml marshaler doesn't use String method. Instead YAML uses encoding.TextMarshaler and encoding.TextUnmarshaler interfaces. Actually, all other codec schemes - JSON, XML, TOML, etc. - use those interfaces to read/write the values. So, if you implement those methods for your type, you will receive all other codecs for free.

Here is an example how to make a human-readable encoding for your enum: https://go.dev/play/p/pEcBmAM-oZJ

type Engine int

const (
    engineUnknown Engine = iota // must be first
    EngineDocker
    engineDone // must be last
)

var engineNames []string
var engineNameToValue map[string]Engine

func init() {
    engineNames = []string{"Unknown", "Docker"}
    engineNameToValue = make(map[string]Engine)
    for i, name := range engineNames {
        engineNameToValue[strings.ToLower(name)] = Engine(i)
    }
}

func (e Engine) String() string {
    if e < 0 || int(e) >= len(engineNames) {
        panic(fmt.Errorf("Invalid engine code: %d", e))
    }
    return engineNames[e]
}

func ParseEngine(text string) (Engine, error) {
    i, ok := engineNameToValue[strings.ToLower(text)]
    if !ok {
        return engineUnknown, fmt.Errorf("Invalid engine name: %s", text)
    }
    return i, nil
}

func (e Engine) MarshalText() ([]byte, error) {
    return []byte(e.String()), nil
}

func (e *Engine) UnmarshalText(text []byte) (err error) {
    name := string(text)
    *e, err = ParseEngine(name)
    return
}

How it works:

func main() {
    j := Job{Engine: EngineDocker}

    fmt.Printf("%#v\n\n", j)
    out, err := yaml.Marshal(j)
    if err != nil {
        panic(err)
    }
    fmt.Printf("YAML: %s\n", string(out))

    var jj Job
    err = yaml.Unmarshal(out, &jj)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n\n", jj)

    // == JSON ==
    out, err = json.Marshal(j)
    if err != nil {
        panic(err)
    }
    fmt.Printf("JSON: %s\n", string(out))

    var jjs Job
    err = json.Unmarshal(out, &jjs)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n\n", jjs)

}

the output

main.Job{Engine:1}

YAML: Engine: Docker

main.Job{Engine:1}

JSON: {"Engine":"Docker"}
main.Job{Engine:1}

See? It writes and reads strings to both YAML and JSON without any extra effort.

  • Related