From ba1a59399f205f62807f110bb3f7f0ef4f53e47c Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 6 Jun 2026 09:01:57 -0400 Subject: [PATCH 1/5] feat: add Flag.SchemaType() and Flag.SchemaItemsType() for JSON Schema introspection --- flag.go | 17 ++++ flag_bool_with_inverse.go | 8 ++ flag_ext.go | 8 ++ flag_impl.go | 48 ++++++++++++ flag_schema_type_test.go | 161 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 flag_schema_type_test.go diff --git a/flag.go b/flag.go index 1b849f1228..3cb9ab9608 100644 --- a/flag.go +++ b/flag.go @@ -151,6 +151,23 @@ type DocGenerationMultiValueFlag interface { IsMultiValueFlag() bool } +// SchemaTyper is an optional interface for flags that can report their +// JSON Schema type for programmatic introspection. +type SchemaTyper interface { + // SchemaType returns the JSON Schema type name for the value this + // flag accepts: "boolean", "integer", "number", "string", "array", + // "object". Returns "" if the flag does not map cleanly. + SchemaType() string +} + +// SchemaItemsTyper is an optional interface for multi-value flags that +// can report the JSON Schema type of their elements. +type SchemaItemsTyper interface { + // SchemaItemsType returns the JSON Schema type of elements for + // array-type flags. Returns "" for single-value or object flags. + SchemaItemsType() string +} + // Countable is an interface to enable detection of flag values which support // repetitive flags type Countable interface { diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index bc12c25a25..199d22c7d9 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -263,3 +263,11 @@ func (bif *BoolWithInverseFlag) IsDefaultVisible() bool { func (bif *BoolWithInverseFlag) TypeName() string { return "bool" } + +func (bif *BoolWithInverseFlag) SchemaType() string { + return "boolean" +} + +func (bif *BoolWithInverseFlag) SchemaItemsType() string { + return "" +} diff --git a/flag_ext.go b/flag_ext.go index 9972af7c56..25db27f16d 100644 --- a/flag_ext.go +++ b/flag_ext.go @@ -61,3 +61,11 @@ func (e *extFlag) GetDefaultText() string { func (e *extFlag) GetEnvVars() []string { return nil } + +func (e *extFlag) SchemaType() string { + return "" +} + +func (e *extFlag) SchemaItemsType() string { + return "" +} diff --git a/flag_impl.go b/flag_impl.go index ab97da8738..8b466b7a84 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "strings" + "time" ) // Value represents a value as used by cli. @@ -285,6 +286,53 @@ func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error { return nil } +// SchemaType returns the JSON Schema type for the flag's value type. +func (f *FlagBase[T, C, V]) SchemaType() string { + var zero T + switch any(zero).(type) { + case bool: + return "boolean" + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return "integer" + case float32, float64: + return "number" + case string: + return "string" + case time.Duration: + return "string" + case time.Time: + return "string" + case []string, []int, []int8, []int16, []int32, []int64, + []uint, []uint8, []uint16, []uint32, []uint64, + []float32, []float64: + return "array" + case map[string]string: + return "object" + default: + return "" + } +} + +// SchemaItemsType returns the JSON Schema element type for slice flags. +func (f *FlagBase[T, C, V]) SchemaItemsType() string { + var zero T + t := reflect.TypeOf(zero) + if t.Kind() == reflect.Slice { + switch t.Elem().Kind() { + case reflect.Bool: + return "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "integer" + case reflect.Float32, reflect.Float64: + return "number" + case reflect.String: + return "string" + } + } + return "" +} + // IsMultiValueFlag returns true if the value type T can take multiple // values from cmd line. This is true for slice and map type flags func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool { diff --git a/flag_schema_type_test.go b/flag_schema_type_test.go new file mode 100644 index 0000000000..c5e81f55e2 --- /dev/null +++ b/flag_schema_type_test.go @@ -0,0 +1,161 @@ +package cli + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestFlag_SchemaType_Bool(t *testing.T) { + f := &BoolFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "boolean", st.SchemaType()) + _, ok = any(f).(SchemaItemsTyper) + assert.True(t, ok) +} + +func TestFlag_SchemaType_String(t *testing.T) { + f := &StringFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "string", st.SchemaType()) + _, ok = any(f).(SchemaItemsTyper) + assert.True(t, ok) +} + +func TestFlag_SchemaType_Int(t *testing.T) { + flags := []Flag{&IntFlag{}, &Int8Flag{}, &Int16Flag{}, &Int32Flag{}, &Int64Flag{}} + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "integer", st.SchemaType()) + } +} + +func TestFlag_SchemaType_Uint(t *testing.T) { + flags := []Flag{&UintFlag{}, &Uint8Flag{}, &Uint16Flag{}, &Uint32Flag{}, &Uint64Flag{}} + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "integer", st.SchemaType()) + } +} + +func TestFlag_SchemaType_Float(t *testing.T) { + flags := []Flag{&FloatFlag{}, &Float32Flag{}, &Float64Flag{}} + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "number", st.SchemaType()) + } +} + +func TestFlag_SchemaType_Duration(t *testing.T) { + f := &DurationFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "string", st.SchemaType()) +} + +func TestFlag_SchemaType_Timestamp(t *testing.T) { + f := &TimestampFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "string", st.SchemaType()) +} + +func TestFlag_SchemaType_Slice(t *testing.T) { + flags := []Flag{ + &StringSliceFlag{}, + &IntSliceFlag{}, + &FloatSliceFlag{}, + } + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "array", st.SchemaType()) + } +} + +func TestFlag_SchemaItemsType_Slice(t *testing.T) { + tests := []struct { + flag Flag + itemType string + }{ + {&StringSliceFlag{}, "string"}, + {&IntSliceFlag{}, "integer"}, + {&Int8SliceFlag{}, "integer"}, + {&Int16SliceFlag{}, "integer"}, + {&Int32SliceFlag{}, "integer"}, + {&Int64SliceFlag{}, "integer"}, + {&UintSliceFlag{}, "integer"}, + {&Uint8SliceFlag{}, "integer"}, + {&Uint16SliceFlag{}, "integer"}, + {&Uint32SliceFlag{}, "integer"}, + {&Uint64SliceFlag{}, "integer"}, + {&FloatSliceFlag{}, "number"}, + {&Float32SliceFlag{}, "number"}, + {&Float64SliceFlag{}, "number"}, + } + for _, tc := range tests { + t.Run("", func(t *testing.T) { + st, ok := tc.flag.(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, tc.itemType, st.SchemaItemsType()) + }) + } +} + +func TestFlag_SchemaType_Map(t *testing.T) { + f := &StringMapFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "object", st.SchemaType()) + sit, ok := any(f).(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, "", sit.SchemaItemsType()) +} + +func TestFlag_SchemaType_Generic(t *testing.T) { + f := &GenericFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "", st.SchemaType()) +} + +func TestFlag_SchemaType_BoolWithInverse(t *testing.T) { + f := &BoolWithInverseFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "boolean", st.SchemaType()) + sit, ok := any(f).(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, "", sit.SchemaItemsType()) +} + +func TestFlag_SchemaType_NonSliceItemsType(t *testing.T) { + flags := []Flag{ + &BoolFlag{}, + &StringFlag{}, + &IntFlag{}, + &FloatFlag{}, + &DurationFlag{}, + &TimestampFlag{}, + } + for _, f := range flags { + sit, ok := f.(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, "", sit.SchemaItemsType()) + } +} + +func TestFlag_SchemaType_PreservesPrecision(t *testing.T) { + created := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + f := &TimestampFlag{Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Value: created} + assert.Equal(t, "string", f.SchemaType()) + + f2 := &DurationFlag{Value: 5 * time.Second} + assert.Equal(t, "string", f2.SchemaType()) +} From e0804d30784959e9ec1abfbd5c57356659c87872 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 6 Jun 2026 09:13:26 -0400 Subject: [PATCH 2/5] Add docs --- godoc-current.txt | 27 +++++++++++++++++++++++++++ testdata/godoc-v3.x.txt | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/godoc-current.txt b/godoc-current.txt index 681df19d63..ff52914972 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -405,6 +405,10 @@ func (bif *BoolWithInverseFlag) PreParse() error func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error +func (bif *BoolWithInverseFlag) SchemaItemsType() string + +func (bif *BoolWithInverseFlag) SchemaType() string + func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) @@ -1032,6 +1036,12 @@ func (f *FlagBase[T, C, V]) PreParse() error func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set +func (f *FlagBase[T, C, V]) SchemaItemsType() string + SchemaItemsType returns the JSON Schema element type for slice flags. + +func (f *FlagBase[T, C, V]) SchemaType() string + SchemaType returns the JSON Schema type for the flag's value type. + func (f *FlagBase[T, C, V]) Set(_ string, val string) error Set applies given value from string @@ -1296,6 +1306,23 @@ type RequiredFlag interface { it allows flags required flags to be backwards compatible with the Flag interface +type SchemaItemsTyper interface { + // SchemaItemsType returns the JSON Schema type of elements for + // array-type flags. Returns "" for single-value or object flags. + SchemaItemsType() string +} + SchemaItemsTyper is an optional interface for multi-value flags that can + report the JSON Schema type of their elements. + +type SchemaTyper interface { + // SchemaType returns the JSON Schema type name for the value this + // flag accepts: "boolean", "integer", "number", "string", "array", + // "object". Returns "" if the flag does not map cleanly. + SchemaType() string +} + SchemaTyper is an optional interface for flags that can report their JSON + Schema type for programmatic introspection. + type Serializer interface { Serialize() string } diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 681df19d63..ff52914972 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -405,6 +405,10 @@ func (bif *BoolWithInverseFlag) PreParse() error func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error +func (bif *BoolWithInverseFlag) SchemaItemsType() string + +func (bif *BoolWithInverseFlag) SchemaType() string + func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) @@ -1032,6 +1036,12 @@ func (f *FlagBase[T, C, V]) PreParse() error func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set +func (f *FlagBase[T, C, V]) SchemaItemsType() string + SchemaItemsType returns the JSON Schema element type for slice flags. + +func (f *FlagBase[T, C, V]) SchemaType() string + SchemaType returns the JSON Schema type for the flag's value type. + func (f *FlagBase[T, C, V]) Set(_ string, val string) error Set applies given value from string @@ -1296,6 +1306,23 @@ type RequiredFlag interface { it allows flags required flags to be backwards compatible with the Flag interface +type SchemaItemsTyper interface { + // SchemaItemsType returns the JSON Schema type of elements for + // array-type flags. Returns "" for single-value or object flags. + SchemaItemsType() string +} + SchemaItemsTyper is an optional interface for multi-value flags that can + report the JSON Schema type of their elements. + +type SchemaTyper interface { + // SchemaType returns the JSON Schema type name for the value this + // flag accepts: "boolean", "integer", "number", "string", "array", + // "object". Returns "" if the flag does not map cleanly. + SchemaType() string +} + SchemaTyper is an optional interface for flags that can report their JSON + Schema type for programmatic introspection. + type Serializer interface { Serialize() string } From 9d8c2c70a6f3929acdc458b2d9d20445c73d9dc3 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 6 Jun 2026 18:21:15 -0400 Subject: [PATCH 3/5] test: add extFlag SchemaType coverage, remove unreachable bool slice branch --- flag_impl.go | 2 -- flag_test.go | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flag_impl.go b/flag_impl.go index 8b466b7a84..b6c7697cf7 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -319,8 +319,6 @@ func (f *FlagBase[T, C, V]) SchemaItemsType() string { t := reflect.TypeOf(zero) if t.Kind() == reflect.Slice { switch t.Elem().Kind() { - case reflect.Bool: - return "boolean" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return "integer" diff --git a/flag_test.go b/flag_test.go index 7d7a80b359..955344c6ca 100644 --- a/flag_test.go +++ b/flag_test.go @@ -3312,6 +3312,9 @@ func TestExtFlag(t *testing.T) { assert.Equal(t, "11", extF.GetValue()) assert.Equal(t, "10", extF.GetDefaultText()) assert.Nil(t, extF.GetEnvVars()) + + assert.Equal(t, "", extF.SchemaType()) + assert.Equal(t, "", extF.SchemaItemsType()) } func TestSliceValuesNil(t *testing.T) { From c4990ee14d2a852ee895b450ff24d2890172dc22 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 13 Jun 2026 16:55:46 -0400 Subject: [PATCH 4/5] feat: implement review feedback for SchemaType on extFlag Use e.Get() type switch to determine the JSON Schema type for extFlag instead of always returning empty string. --- flag_ext.go | 22 ++++++++++++++++++++-- flag_impl.go | 4 ++-- flag_schema_type_test.go | 8 ++++---- flag_test.go | 2 +- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/flag_ext.go b/flag_ext.go index 25db27f16d..3fb01f2597 100644 --- a/flag_ext.go +++ b/flag_ext.go @@ -1,6 +1,9 @@ package cli -import "flag" +import ( + "flag" + "time" +) type extFlag struct { f *flag.Flag @@ -63,7 +66,22 @@ func (e *extFlag) GetEnvVars() []string { } func (e *extFlag) SchemaType() string { - return "" + switch e.Get().(type) { + case bool: + return "boolean" + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return "integer" + case float32, float64: + return "number" + case string: + return "string" + case time.Duration: + return "duration" + case time.Time: + return "date-time" + default: + return "" + } } func (e *extFlag) SchemaItemsType() string { diff --git a/flag_impl.go b/flag_impl.go index b6c7697cf7..a67208c91c 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -299,9 +299,9 @@ func (f *FlagBase[T, C, V]) SchemaType() string { case string: return "string" case time.Duration: - return "string" + return "duration" case time.Time: - return "string" + return "date-time" case []string, []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []float32, []float64: diff --git a/flag_schema_type_test.go b/flag_schema_type_test.go index c5e81f55e2..07b8474a5b 100644 --- a/flag_schema_type_test.go +++ b/flag_schema_type_test.go @@ -56,14 +56,14 @@ func TestFlag_SchemaType_Duration(t *testing.T) { f := &DurationFlag{} st, ok := any(f).(SchemaTyper) assert.True(t, ok) - assert.Equal(t, "string", st.SchemaType()) + assert.Equal(t, "duration", st.SchemaType()) } func TestFlag_SchemaType_Timestamp(t *testing.T) { f := &TimestampFlag{} st, ok := any(f).(SchemaTyper) assert.True(t, ok) - assert.Equal(t, "string", st.SchemaType()) + assert.Equal(t, "date-time", st.SchemaType()) } func TestFlag_SchemaType_Slice(t *testing.T) { @@ -154,8 +154,8 @@ func TestFlag_SchemaType_NonSliceItemsType(t *testing.T) { func TestFlag_SchemaType_PreservesPrecision(t *testing.T) { created := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) f := &TimestampFlag{Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Value: created} - assert.Equal(t, "string", f.SchemaType()) + assert.Equal(t, "date-time", f.SchemaType()) f2 := &DurationFlag{Value: 5 * time.Second} - assert.Equal(t, "string", f2.SchemaType()) + assert.Equal(t, "duration", f2.SchemaType()) } diff --git a/flag_test.go b/flag_test.go index 955344c6ca..0cf54de05f 100644 --- a/flag_test.go +++ b/flag_test.go @@ -3313,7 +3313,7 @@ func TestExtFlag(t *testing.T) { assert.Equal(t, "10", extF.GetDefaultText()) assert.Nil(t, extF.GetEnvVars()) - assert.Equal(t, "", extF.SchemaType()) + assert.Equal(t, "integer", extF.SchemaType()) assert.Equal(t, "", extF.SchemaItemsType()) } From 4c9035753374883467f5f828f107bed6185d7eb3 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 13 Jun 2026 17:06:45 -0400 Subject: [PATCH 5/5] test: add extFlag SchemaType coverage for all type branches Covers bool, string, float64, duration, timestamp, and unknown types to satisfy codecov patch check. --- flag_schema_type_test.go | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/flag_schema_type_test.go b/flag_schema_type_test.go index 07b8474a5b..6870730f99 100644 --- a/flag_schema_type_test.go +++ b/flag_schema_type_test.go @@ -1,6 +1,7 @@ package cli import ( + "flag" "testing" "time" @@ -125,6 +126,79 @@ func TestFlag_SchemaType_Generic(t *testing.T) { assert.Equal(t, "", st.SchemaType()) } +func TestExtFlag_SchemaType(t *testing.T) { + tests := []struct { + name string + makeFlag func() *extFlag + schemaType string + }{ + { + name: "bool", + makeFlag: func() *extFlag { + var bv boolValue + var p bool + return &extFlag{f: &flag.Flag{Value: bv.Create(true, &p, BoolConfig{})}} + }, + schemaType: "boolean", + }, + { + name: "string", + makeFlag: func() *extFlag { + var sv stringValue + var p string + return &extFlag{f: &flag.Flag{Value: sv.Create("hi", &p, StringConfig{})}} + }, + schemaType: "string", + }, + { + name: "float64", + makeFlag: func() *extFlag { + var fv floatValue[float64] + var p float64 + return &extFlag{f: &flag.Flag{Value: fv.Create(3.14, &p, NoConfig{})}} + }, + schemaType: "number", + }, + { + name: "duration", + makeFlag: func() *extFlag { + var dv durationValue + var p time.Duration + return &extFlag{f: &flag.Flag{Value: dv.Create(5*time.Second, &p, NoConfig{})}} + }, + schemaType: "duration", + }, + { + name: "timestamp", + makeFlag: func() *extFlag { + var tv timestampValue + var p time.Time + return &extFlag{f: &flag.Flag{Value: tv.Create(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), &p, TimestampConfig{})}} + }, + schemaType: "date-time", + }, + { + name: "unknown", + makeFlag: func() *extFlag { + return &extFlag{f: &flag.Flag{Value: &customValue{}}} + }, + schemaType: "", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.schemaType, tc.makeFlag().SchemaType()) + assert.Equal(t, "", tc.makeFlag().SchemaItemsType()) + }) + } +} + +type customValue struct{} + +func (c *customValue) String() string { return "custom" } +func (c *customValue) Set(string) error { return nil } +func (c *customValue) Get() any { return struct{}{} } + func TestFlag_SchemaType_BoolWithInverse(t *testing.T) { f := &BoolWithInverseFlag{} st, ok := any(f).(SchemaTyper)