Marshal a struct-tagged Go value to JSON (e.g. mapstructure-tagged) — reading the tags themselves, not a parallel set of json tags.
type Settings struct {
TabLen int `mapstructure:"tab-len"`
Fast bool `mapstructure:"fast"`
}
func main() {
m := tagjson.NewMarshaler("mapstructure")
b, err := m.Marshal(Settings{TabLen: 4, Fast: false})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
// Output: {"tab-len":4}
}mapstructure is decode-only: it reads a plain map (typically itself decoded
from YAML or JSON) into a Go struct tagged for it, but has no marshal
direction of its own. Handing such a struct to encoding/json directly
ignores the mapstructure tags entirely, falling back to raw Go field names
and writing every zero value out explicitly.
Marshal reads the mapstructure tag through reflection instead, so any
type already set up for mapstructure decoding marshals back out correctly
with no further annotation — including a type added after this package was
last touched, since there is no per-type registry to fall out of sync.
Marshal and MarshalWrite always read mapstructure, matching the
package name. For any other tag — yaml, toml, a project's own — use
Marshaler directly, with the same semantics throughout:
type Settings struct {
TabLen int `yaml:"tab-len"`
Fast bool `yaml:"fast"`
}
func main() {
m := tagjson.NewMarshaler("yaml")
b, err := m.Marshal(Settings{TabLen: 4, Fast: false})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
// Output: {"tab-len":4}
}This package only produces JSON. A caller wanting YAML converts Marshal's output
with whichever library they prefer, rather than this package choosing one and
every consumer paying for it:
import (
"encoding/json/jsontext"
"fmt"
"os"
"github.com/MarkRosemaker/json2yaml"
"github.com/MarkRosemaker/tagjson"
"gopkg.in/yaml.v3"
)
type Settings struct {
TabLen int `yaml:"tab-len"`
Fast bool `yaml:"fast"`
}
func Example_foo() {
m := tagjson.NewMarshaler("yaml")
b, _ := m.Marshal(Settings{TabLen: 4, Fast: false})
node, _ := json2yaml.Convert(jsontext.Value(b))
_ = yaml.NewEncoder(os.Stdout).Encode(node)
// Output: tab-len: 4
}The mapping follows mapstructure's own rules, in reverse:
- A field tagged
"-", or carrying nomapstructuretag at all, is somethingmapstructurewould not have populated from a decoded map either, and is omitted. - A field tagged
",squash"is an embedded struct whose own fields are inlined into the enclosing object, rather than nested under a key of their own —mapstructure's flattening, run the other direction. - Every other tag names the field's key verbatim.
- A field left at its zero value — an empty string, an unset bool, a nil or empty slice or map, a struct whose own fields are all likewise empty — is omitted.
- Map keys are written as-is, sorted, since they're caller-chosen names (not fixed fields) and a Go map has no order of its own.
time.Durationis written as itsString()form ("1h30m"), not as a raw number of nanoseconds.
There is no way, from the mapstructure tag alone, to know whether a
particular type's zero value happens to be its meaningful default. A type
where that distinction matters — some field defaults to true, say, so an
explicit false must never be confused with "unset" — needs a hand-written
marshaler for that one type instead of this package's generic, reflection-driven
one. See lintconfig for exactly that: the same problem,
solved by hand for golangci-lint's config type, field by field, with the
build breaking instead of drifting silently when golangci-lint's own types
change shape.
- Go Reference: API documentation.
Contributions are welcome — please open an issue or a pull request on GitHub.
This project is licensed under the Apache 2.0 License.