MarkRosemaker/tagjson

★ 0Forks 0GoGitHub ↗Compare

README

Go Reference Code Coverage License

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}
}

Why this exists

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.

Reading a different tag

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}
}

Getting YAML

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
}

Semantics

The mapping follows mapstructure's own rules, in reverse:

  • A field tagged "-", or carrying no mapstructure tag at all, is something mapstructure would 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.Duration is written as its String() form ("1h30m"), not as a raw number of nanoseconds.

What this package can't do

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.

Additional Information

Contributing

Contributions are welcome — please open an issue or a pull request on GitHub.

License

This project is licensed under the Apache 2.0 License.

Contributors

MarkRosemaker

Issues