Repository: alecthomas/kong
Pull request #178 (2021-06-18 18:34:58)

Added sub objects support in JSON resolver
Added support of sub objects to JSON resolver.

alecthomas: Unfortunately this wouldn't be backward compatible, so I won't be able to merge this. If you need this behaviour I'd just copy the JSON resolver code and modify it.
yazver: I can change it for compatibility:

```go
func JSON(r io.Reader) (Resolver, error) {
	values := map[string]interface{}{}
	err := json.NewDecoder(r).Decode(&values)
	if err != nil {
		return nil, err
	}
	var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (interface{}, error) {
		name := strings.ReplaceAll(flag.Name, "-", "_")
		raw, ok := values[name]
		if ok {
			return raw, nil
		}
		raw = values
		for _, part := range strings.Split(name, ".") {
			if values, ok := raw.(map[string]interface{}); ok {
				raw, ok = values[part]
				if !ok {
					return nil, nil
				}
			} else {
				return nil, nil
			}
		}
		return raw, nil
	}

	return f, nil
} 
```
yazver: I can change it for compatibility:

```go
func JSON(r io.Reader) (Resolver, error) {
	values := map[string]interface{}{}
	err := json.NewDecoder(r).Decode(&values)
	if err != nil {
		return nil, err
	}
	var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (interface{}, error) {
		name := strings.ReplaceAll(flag.Name, "-", "_")
		raw, ok := values[name]
		if ok {
			return raw, nil
		}
		raw = values
		for _, part := range strings.Split(name, ".") {
			if values, ok := raw.(map[string]interface{}); ok {
				raw, ok = values[part]
				if !ok {
					return nil, nil
				}
			} else {
				return nil, nil
			}
		}
		return raw, nil
	}

	return f, nil
}
```


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 0d2c6f8..0f7ad82 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -34,6 +34,7 @@ linters:
     - interfacer
     - tagliatelle
     - thelper
+    - godox
 
 linters-settings:
   govet:
diff --git a/README.md b/README.md
index 6339210..03996db 100644
--- a/README.md
+++ b/README.md
@@ -444,9 +444,9 @@ Tag                    | Description
 `format:"X"`           | Format for parsing input, if supported.
 `sep:"X"`              | Separator for sequences (defaults to ","). May be `none` to disable splitting.
 `mapsep:"X"`           | Separator for maps (defaults to ";"). May be `none` to disable splitting.
-`enum:"X,Y,..."`       | Set of valid values allowed for this flag.
+`enum:"X,Y,..."`       | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`.
 `group:"X"`            | Logical group for a flag or command.
-`xor:"X"`              | Exclusive OR group for flags. Only one flag in the group can be used which is restricted within the same command.
+`xor:"X,Y,..."`        | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required.
 `prefix:"X"`           | Prefix for all sub-flags.
 `set:"K=V"`            | Set a variable for expansion by child elements. Multiples can occur.
 `embed:""`             | If present, this field's children will be embedded in the parent. Useful for composition.
diff --git a/build.go b/build.go
index d2ca41c..22f9839 100644
--- a/build.go
+++ b/build.go
@@ -51,7 +51,7 @@ func flattenedFields(v reflect.Value) (out []flattenedField) {
 	for i := 0; i < v.NumField(); i++ {
 		ft := v.Type().Field(i)
 		fv := v.Field(i)
-		tag := parseTag(fv, ft)
+		tag := parseTag(v, fv, ft)
 		if tag.Ignored {
 			continue
 		}
@@ -156,8 +156,7 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
 	// a positional argument is provided to the child, and move it to the branching argument field.
 	if tag.Arg {
 		if len(child.Positional) == 0 {
-			fail("positional branch %s.%s must have at least one child positional argument named %q",
-				v.Type().Name(), ft.Name, name)
+			failField(v, ft, "positional branch must have at least one child positional argument named %q", name)
 		}
 
 		value := child.Positional[0]
@@ -168,8 +167,7 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
 
 		child.Name = value.Name
 		if child.Name != name {
-			fail("first field in positional branch %s.%s must have the same name as the parent field (%s).",
-				v.Type().Name(), ft.Name, child.Name)
+			failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
 		}
 
 		child.Argument = value
@@ -179,14 +177,14 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
 	node.Children = append(node.Children, child)
 
 	if len(child.Positional) > 0 && len(child.Children) > 0 {
-		fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name)
+		failField(v, ft, "can't mix positional arguments and branching arguments")
 	}
 }
 
 func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
 	mapper := k.registry.ForNamedValue(tag.Type, fv)
 	if mapper == nil {
-		fail("unsupported field type %s.%s (of type %s), perhaps missing a cmd:\"\" tag?", v.Type(), ft.Name, ft.Type)
+		failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type)
 	}
 
 	value := &Value{
@@ -209,13 +207,13 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
 		node.Positional = append(node.Positional, value)
 	} else {
 		if seenFlags["--"+value.Name] {
-			fail("duplicate flag --%s", value.Name)
+			failField(v, ft, "duplicate flag --%s", value.Name)
 		} else {
 			seenFlags["--"+value.Name] = true
 		}
 		if tag.Short != 0 {
 			if seenFlags["-"+string(tag.Short)] {
-				fail("duplicate short flag -%c", tag.Short)
+				failField(v, ft, "duplicate short flag -%c", tag.Short)
 			} else {
 				seenFlags["-"+string(tag.Short)] = true
 			}
diff --git a/context.go b/context.go
index 1e5ae75..8065efd 100644
--- a/context.go
+++ b/context.go
@@ -743,17 +743,32 @@ func (c *Context) PrintUsage(summary bool) error {
 }
 
 func checkMissingFlags(flags []*Flag) error {
+	xorGroup := map[string][]string{}
 	missing := []string{}
 	for _, flag := range flags {
 		if !flag.Required || flag.Set {
 			continue
 		}
-		missing = append(missing, flag.Summary())
+		if len(flag.Xor) > 0 {
+			for _, xor := range flag.Xor {
+				xorGroup[xor] = append(xorGroup[xor], flag.Summary())
+			}
+		} else {
+			missing = append(missing, flag.Summary())
+		}
+	}
+	for _, flags := range xorGroup {
+		if len(flags) > 1 {
+			missing = append(missing, strings.Join(flags, " or "))
+		}
 	}
+
 	if len(missing) == 0 {
 		return nil
 	}
 
+	sort.Strings(missing)
+
 	return fmt.Errorf("missing flags: %s", strings.Join(missing, ", "))
 }
 
@@ -820,7 +835,18 @@ func checkMissingPositionals(positional int, values []*Value) error {
 
 	missing := []string{}
 	for ; positional < len(values); positional++ {
-		missing = append(missing, "<"+values[positional].Name+">")
+		arg := values[positional]
+		// TODO(aat): Fix hardcoding of these env checks all over the place :\
+		if arg.Tag.Env != "" {
+			_, ok := os.LookupEnv(arg.Tag.Env)
+			if ok {
+				continue
+			}
+		}
+		missing = append(missing, "<"+arg.Name+">")
+	}
+	if len(missing) == 0 {
+		return nil
 	}
 	return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " "))
 }
@@ -860,13 +886,12 @@ func checkXorDuplicates(paths []*Path) error {
 			if !flag.Set {
 				continue
 			}
-			if flag.Xor == "" {
-				continue
-			}
-			if seen[flag.Xor] != nil {
-				return fmt.Errorf("--%s and --%s can't be used together", seen[flag.Xor].Name, flag.Name)
+			for _, xor := range flag.Xor {
+				if seen[xor] != nil {
+					return fmt.Errorf("--%s and --%s can't be used together", seen[xor].Name, flag.Name)
+				}
+				seen[xor] = flag
 			}
-			seen[flag.Xor] = flag
 		}
 	}
 	return nil
diff --git a/kong.go b/kong.go
index f02b867..3edfc3b 100644
--- a/kong.go
+++ b/kong.go
@@ -22,6 +22,15 @@ func fail(format string, args ...interface{}) {
 	panic(Error{msg: fmt.Sprintf(format, args...)})
 }
 
+func failField(parent reflect.Value, field reflect.StructField, format string, args ...interface{}) {
+	name := parent.Type().Name()
+	if name == "" {
+		name = "<anonymous struct>"
+	}
+	msg := fmt.Sprintf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...))
+	panic(Error{msg: msg})
+}
+
 // Must creates a new Parser or panics if there is an error.
 func Must(ast interface{}, options ...Option) *Kong {
 	k, err := New(ast, options...)
diff --git a/model.go b/model.go
index 928c613..450f61c 100644
--- a/model.go
+++ b/model.go
@@ -377,7 +377,7 @@ type Positional = Value
 type Flag struct {
 	*Value
 	Group       *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
-	Xor         string
+	Xor         []string
 	PlaceHolder string
 	Env         string
 	Short       rune
diff --git a/options.go b/options.go
index 98620c5..57a8f13 100644
--- a/options.go
+++ b/options.go
@@ -371,3 +371,56 @@ func ExpandPath(path string) string {
 	}
 	return abspath
 }
+
+func siftStrings(ss []string, filter func(s string) bool) []string {
+	i := 0
+	ss = append([]string(nil), ss...)
+	for _, s := range ss {
+		if filter(s) {
+			ss[i] = s
+			i++
+		}
+	}
+	return ss[0:i]
+}
+
+// DefaultEnvars option inits environment names for flags.
+// The name will not generate if tag "env" is "-".
+// Predefined environment variables are skipped.
+//
+// For example:
+//   --some.value -> PREFIX_SOME_VALUE
+func DefaultEnvars(prefix string) Option {
+	processFlag := func(flag *Flag) {
+		switch env := flag.Env; {
+		case flag.Name == "help":
+			return
+		case env == "-":
+			flag.Env = ""
+			return
+		case env != "":
+			return
+		}
+		replacer := strings.NewReplacer("-", "_", ".", "_")
+		names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
+		names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
+		name := strings.ToUpper(strings.Join(names, "_"))
+		flag.Env = name
+		flag.Value.Tag.Env = name
+	}
+
+	var processNode func(node *Node)
+	processNode = func(node *Node) {
+		for _, flag := range node.Flags {
+			processFlag(flag)
+		}
+		for _, node := range node.Children {
+			processNode(node)
+		}
+	}
+
+	return PostBuild(func(k *Kong) error {
+		processNode(k.Model.Node)
+		return nil
+	})
+}
diff --git a/resolver.go b/resolver.go
index 623a734..2fa626f 100644
--- a/resolver.go
+++ b/resolver.go
@@ -39,8 +39,19 @@ func JSON(r io.Reader) (Resolver, error) {
 	var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (interface{}, error) {
 		name := strings.ReplaceAll(flag.Name, "-", "_")
 		raw, ok := values[name]
-		if !ok {
-			return nil, nil
+		if ok {
+			return raw, nil
+		}
+		raw = values
+		for _, part := range strings.Split(name, ".") {
+			if values, ok := raw.(map[string]interface{}); ok {
+				raw, ok = values[part]
+				if !ok {
+					return nil, nil
+				}
+			} else {
+				return nil, nil
+			}
 		}
 		return raw, nil
 	}
diff --git a/tag.go b/tag.go
index 8c24a9f..7c9ae99 100644
--- a/tag.go
+++ b/tag.go
@@ -29,7 +29,7 @@ type Tag struct {
 	MapSep      rune
 	Enum        string
 	Group       string
-	Xor         string
+	Xor         []string
 	Vars        Vars
 	Prefix      string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
 	Embed       bool
@@ -125,7 +125,11 @@ func newEmptyTag() *Tag {
 	return &Tag{items: map[string][]string{}}
 }
 
-func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
+func tagSplitFn(r rune) bool {
+	return r == ',' || r == ' '
+}
+
+func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag {
 	if ft.Tag.Get("kong") == "-" {
 		t := newEmptyTag()
 		t.Ignored = true
@@ -142,7 +146,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
 	required := t.Has("required")
 	optional := t.Has("optional")
 	if required && optional {
-		fail("can't specify both required and optional")
+		failField(parent, ft, "can't specify both required and optional")
 	}
 	t.Required = required
 	t.Optional = optional
@@ -157,33 +161,32 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
 	t.Env = t.Get("env")
 	t.Short, err = t.GetRune("short")
 	if err != nil && t.Get("short") != "" {
-		fail("invalid short flag name %q: %s", t.Get("short"), err)
+		failField(parent, ft, "invalid short flag name %q: %s", t.Get("short"), err)
 	}
 	t.Hidden = t.Has("hidden")
 	t.Format = t.Get("format")
 	t.Sep, _ = t.GetSep("sep", ',')
 	t.MapSep, _ = t.GetSep("mapsep", ';')
 	t.Group = t.Get("group")
-	t.Xor = t.Get("xor")
+	for _, xor := range t.GetAll("xor") {
+		t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...)
+	}
 	t.Prefix = t.Get("prefix")
 	t.Embed = t.Has("embed")
 	negatable := t.Has("negatable")
 	if negatable && ft.Type.Kind() != reflect.Bool {
-		fail("negatable can only be set on booleans")
+		failField(parent, ft, "negatable can only be set on booleans")
 	}
 	t.Negatable = negatable
-	splitFn := func(r rune) bool {
-		return r == ',' || r == ' '
-	}
 	aliases := t.Get("aliases")
 	if len(aliases) > 0 {
-		t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, splitFn)...)
+		t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)
 	}
 	t.Vars = Vars{}
 	for _, set := range t.GetAll("set") {
 		parts := strings.SplitN(set, "=", 2)
 		if len(parts) == 0 {
-			fail("set should be in the form key=value but got %q", set)
+			failField(parent, ft, "set should be in the form key=value but got %q", set)
 		}
 		t.Vars[parts[0]] = parts[1]
 	}
@@ -192,9 +195,12 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
 		t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
 	}
 	t.Enum = t.Get("enum")
+	if t.Enum != "" && !(t.Required || t.Default != "") {
+		failField(parent, ft, "enum value is only valid if it is either required or has a valid default value")
+	}
 	passthrough := t.Has("passthrough")
 	if passthrough && !t.Arg {
-		fail("passthrough only makes sense for positional arguments")
+		failField(parent, ft, "passthrough only makes sense for positional arguments")
 	}
 	t.Passthrough = passthrough
 	return t


Test patch:
diff --git a/config_test.go b/config_test.go
index 69e279d..a63205b 100644
--- a/config_test.go
+++ b/config_test.go
@@ -32,7 +32,7 @@ func TestMultipleConfigLoading(t *testing.T) {
 
 func TestConfigValidation(t *testing.T) {
 	var cli struct {
-		Flag string `json:"flag,omitempty" enum:"valid"`
+		Flag string `json:"flag,omitempty" enum:"valid" required:""`
 	}
 
 	cli.Flag = "invalid"
diff --git a/kong_test.go b/kong_test.go
index 66fbe1b..37f7529 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -643,7 +643,7 @@ func TestRun(t *testing.T) {
 func TestInterpolationIntoModel(t *testing.T) {
 	var cli struct {
 		Flag    string `default:"${default}" help:"Help, I need ${somebody}" enum:"${enum}"`
-		EnumRef string `enum:"a,b" help:"One of ${enum}"`
+		EnumRef string `enum:"a,b" required:"" help:"One of ${enum}"`
 	}
 	_, err := kong.New(&cli)
 	require.Error(t, err)
@@ -763,7 +763,7 @@ func TestHooksCalledForDefault(t *testing.T) {
 
 func TestEnum(t *testing.T) {
 	var cli struct {
-		Flag string `enum:"a,b,c"`
+		Flag string `enum:"a,b,c" required:""`
 	}
 	_, err := mustNew(t, &cli).Parse([]string{"--flag", "d"})
 	require.EqualError(t, err, "--flag must be one of \"a\",\"b\",\"c\" but got \"d\"")
@@ -868,6 +868,42 @@ func TestXorChild(t *testing.T) {
 	require.Error(t, err, "--two and --three can't be used together")
 }
 
+func TestMultiXor(t *testing.T) {
+	var cli struct {
+		Hello bool   `xor:"one,two"`
+		One   bool   `xor:"one"`
+		Two   string `xor:"two"`
+	}
+
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"--hello", "--one"})
+	require.EqualError(t, err, "--hello and --one can't be used together")
+
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{"--hello", "--two=foo"})
+	require.EqualError(t, err, "--hello and --two can't be used together")
+}
+
+func TestXorRequired(t *testing.T) {
+	var cli struct {
+		One   bool `xor:"one,two" required:""`
+		Two   bool `xor:"one" required:""`
+		Three bool `xor:"two" required:""`
+		Four  bool `required:""`
+	}
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"--one"})
+	require.EqualError(t, err, "missing flags: --four")
+
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{"--two"})
+	require.EqualError(t, err, "missing flags: --four, --one or --three")
+
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{})
+	require.EqualError(t, err, "missing flags: --four, --one or --three, --one or --two")
+}
+
 func TestEnumSequence(t *testing.T) {
 	var cli struct {
 		State []string `enum:"a,b,c" default:"a"`
@@ -902,10 +938,47 @@ func TestIssue40EnumAcrossCommands(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func TestIssue179(t *testing.T) {
+	type A struct {
+		Enum string `required:"" enum:"1,2"`
+	}
+
+	type B struct{}
+
+	var root struct {
+		A A `cmd`
+		B B `cmd`
+	}
+
+	p := mustNew(t, &root)
+	_, err := p.Parse([]string{"b"})
+	require.NoError(t, err)
+}
+
+func TestIssue153(t *testing.T) {
+	type LsCmd struct {
+		Paths []string `arg required name:"path" help:"Paths to list." env:"CMD_PATHS"`
+	}
+
+	var cli struct {
+		Debug bool `help:"Enable debug mode."`
+
+		Ls LsCmd `cmd help:"List paths."`
+	}
+
+	p, revert := newEnvParser(t, &cli, envMap{
+		"CMD_PATHS": "hello",
+	})
+	defer revert()
+	_, err := p.Parse([]string{"ls"})
+	require.NoError(t, err)
+	require.Equal(t, []string{"hello"}, cli.Ls.Paths)
+}
+
 func TestEnumArg(t *testing.T) {
 	var cli struct {
 		Nested struct {
-			One string `arg:"" enum:"a,b,c"`
+			One string `arg:"" enum:"a,b,c" required:""`
 			Two string `arg:""`
 		} `cmd:""`
 	}
@@ -1069,7 +1142,7 @@ func TestDuplicateShortflags(t *testing.T) {
 		Flag2 bool `short:"t"`
 	}{}
 	_, err := kong.New(&cli)
-	require.EqualError(t, err, "duplicate short flag -t")
+	require.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
 }
 
 func TestDuplicateNestedShortFlags(t *testing.T) {
@@ -1080,5 +1153,5 @@ func TestDuplicateNestedShortFlags(t *testing.T) {
 		} `cmd:""`
 	}{}
 	_, err := kong.New(&cli)
-	require.EqualError(t, err, "duplicate short flag -t")
+	require.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
 }
diff --git a/resolver_test.go b/resolver_test.go
index f601544..2606ad4 100644
--- a/resolver_test.go
+++ b/resolver_test.go
@@ -26,10 +26,10 @@ func tempEnv(env envMap) func() {
 	}
 }
 
-func newEnvParser(t *testing.T, cli interface{}, env envMap) (*kong.Kong, func()) {
+func newEnvParser(t *testing.T, cli interface{}, env envMap, options ...kong.Option) (*kong.Kong, func()) {
 	t.Helper()
 	restoreEnv := tempEnv(env)
-	parser := mustNew(t, cli)
+	parser := mustNew(t, cli, options...)
 	return parser, restoreEnv
 }
 
@@ -92,19 +92,84 @@ func TestEnvarsWithDefault(t *testing.T) {
 	require.Equal(t, "moo", cli.Flag)
 }
 
+func TestEnv(t *testing.T) {
+	type Embed struct {
+		Flag string
+	}
+	type Cli struct {
+		One   Embed `prefix:"one-" embed:""`
+		Two   Embed `prefix:"two." embed:""`
+		Three Embed `prefix:"three_" embed:""`
+		Four  Embed `prefix:"four_" embed:""`
+		Five  bool
+		Six   bool `env:"-"`
+	}
+
+	var cli Cli
+
+	expected := Cli{
+		One:   Embed{Flag: "one"},
+		Two:   Embed{Flag: "two"},
+		Three: Embed{Flag: "three"},
+		Four:  Embed{Flag: "four"},
+		Five:  true,
+	}
+
+	// With the prefix
+	parser, unsetEnvs := newEnvParser(t, &cli, envMap{
+		"KONG_ONE_FLAG":   "one",
+		"KONG_TWO_FLAG":   "two",
+		"KONG_THREE_FLAG": "three",
+		"KONG_FOUR_FLAG":  "four",
+		"KONG_FIVE":       "true",
+		"KONG_SIX":        "true",
+	}, kong.DefaultEnvars("KONG"))
+	defer unsetEnvs()
+
+	_, err := parser.Parse(nil)
+	require.NoError(t, err)
+	require.Equal(t, expected, cli)
+
+	// Without the prefix
+	parser, unsetEnvs = newEnvParser(t, &cli, envMap{
+		"ONE_FLAG":   "one",
+		"TWO_FLAG":   "two",
+		"THREE_FLAG": "three",
+		"FOUR_FLAG":  "four",
+		"FIVE":       "true",
+		"SIX":        "true",
+	}, kong.DefaultEnvars(""))
+	defer unsetEnvs()
+
+	_, err = parser.Parse(nil)
+	require.NoError(t, err)
+	require.Equal(t, expected, cli)
+}
+
 func TestJSONBasic(t *testing.T) {
+	type Embed struct {
+		String string
+	}
+
 	var cli struct {
 		String          string
 		Slice           []int
 		SliceWithCommas []string
 		Bool            bool
+
+		One Embed `prefix:"one." embed:""`
+		Two Embed `prefix:"two." embed:""`
 	}
 
 	json := `{
 		"string": "🍕",
 		"slice": [5, 8],
 		"bool": true,
-		"slice_with_commas": ["a,b", "c"]
+		"slice_with_commas": ["a,b", "c"],
+		"one":{
+			"string": "one value"
+		},
+		"two.string": "two value"
 	}`
 
 	r, err := kong.JSON(strings.NewReader(json))
@@ -116,6 +181,8 @@ func TestJSONBasic(t *testing.T) {
 	require.Equal(t, "🍕", cli.String)
 	require.Equal(t, []int{5, 8}, cli.Slice)
 	require.Equal(t, []string{"a,b", "c"}, cli.SliceWithCommas)
+	require.Equal(t, "one value", cli.One.String)
+	require.Equal(t, "two value", cli.Two.String)
 	require.True(t, cli.Bool)
 }
 
diff --git a/tag_test.go b/tag_test.go
index ae8c38d..ca9f213 100644
--- a/tag_test.go
+++ b/tag_test.go
@@ -198,5 +198,5 @@ func TestInvalidRuneErrors(t *testing.T) {
 		Flag bool `short:"invalid"`
 	}{}
 	_, err := kong.New(&cli)
-	require.EqualError(t, err, "invalid short flag name \"invalid\": invalid rune")
+	require.EqualError(t, err, "<anonymous struct>.Flag: invalid short flag name \"invalid\": invalid rune")
 }