From d505fae97917c19de7e7817440501a457a72987c Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Wed, 30 Jul 2025 18:09:25 +0800 Subject: [PATCH] fix: unmarshal problem on env vars for type env string (#5037) Signed-off-by: kevin Signed-off-by: Kevin Wan --- core/mapping/unmarshaler.go | 18 +++--- core/mapping/unmarshaler_test.go | 99 ++++++++++++++++++++++++++++++++ core/mapping/utils.go | 4 ++ 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go index 48deb9b7c..d6d73e9d2 100644 --- a/core/mapping/unmarshaler.go +++ b/core/mapping/unmarshaler.go @@ -30,9 +30,7 @@ var ( errValueNotSettable = errors.New("value is not settable") errValueNotStruct = errors.New("value type is not struct") keyUnmarshaler = NewUnmarshaler(defaultKeyName) - boolType = reflect.TypeOf(false) durationType = reflect.TypeOf(time.Duration(0)) - stringType = reflect.TypeOf("") cacheKeys = make(map[string][]string) cacheKeysLock sync.Mutex defaultCache = make(map[string]any) @@ -768,23 +766,25 @@ func (u *Unmarshaler) processFieldWithEnvValue(fieldType reflect.Type, value ref } derefType := Deref(fieldType) - switch derefType { - case boolType: + derefKind := derefType.Kind() + switch { + case derefKind == reflect.String: + SetValue(fieldType, value, toReflectValue(derefType, envVal)) + return nil + case derefKind == reflect.Bool: val, err := strconv.ParseBool(envVal) if err != nil { return fmt.Errorf("unmarshal field %q with environment variable, %w", fullName, err) } - SetValue(fieldType, value, reflect.ValueOf(val)) + SetValue(fieldType, value, toReflectValue(derefType, val)) return nil - case durationType: + case derefType == durationType: + // time.Duration is a special case, its derefKind is reflect.Int64. if err := fillDurationValue(fieldType, value, envVal); err != nil { return fmt.Errorf("unmarshal field %q with environment variable, %w", fullName, err) } - return nil - case stringType: - SetValue(fieldType, value, reflect.ValueOf(envVal)) return nil default: return u.processFieldPrimitiveWithJSONNumber(fieldType, value, json.Number(envVal), opts, fullName) diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go index 54f342c62..a0e4f782f 100644 --- a/core/mapping/unmarshaler_test.go +++ b/core/mapping/unmarshaler_test.go @@ -6083,6 +6083,105 @@ func TestParseJsonStringValue(t *testing.T) { }) } +// issue #5033, string type +func TestUnmarshalFromEnvString(t *testing.T) { + t.Setenv("STRING_ENV", "dev") + + t.Run("by value", func(t *testing.T) { + type ( + Env string + Config struct { + Env Env `json:",env=STRING_ENV,default=prod"` + } + ) + + var c Config + if assert.NoError(t, UnmarshalJsonMap(map[string]any{}, &c)) { + assert.Equal(t, Env("dev"), c.Env) + } + }) + + t.Run("by ptr", func(t *testing.T) { + type ( + Env string + Config struct { + Env *Env `json:",env=STRING_ENV,default=prod"` + } + ) + + var c Config + if assert.NoError(t, UnmarshalJsonMap(map[string]any{}, &c)) { + assert.Equal(t, Env("dev"), *c.Env) + } + }) +} + +// issue #5033, bool type +func TestUnmarshalFromEnvBool(t *testing.T) { + t.Setenv("BOOL_ENV", "true") + + t.Run("by value", func(t *testing.T) { + type ( + Env bool + Config struct { + Env Env `json:",env=BOOL_ENV,default=false"` + } + ) + + var c Config + if assert.NoError(t, UnmarshalJsonMap(map[string]any{}, &c)) { + assert.Equal(t, Env(true), c.Env) + } + }) + + t.Run("by ptr", func(t *testing.T) { + type ( + Env bool + Config struct { + Env *Env `json:",env=BOOL_ENV,default=false"` + } + ) + + var c Config + if assert.NoError(t, UnmarshalJsonMap(map[string]any{}, &c)) { + assert.Equal(t, Env(true), *c.Env) + } + }) +} + +// issue #5033, customized int type +func TestUnmarshalFromEnvInt(t *testing.T) { + t.Setenv("INT_ENV", "2") + + t.Run("by value", func(t *testing.T) { + type ( + Env int + Config struct { + Env Env `json:",env=INT_ENV,default=0"` + } + ) + + var c Config + if assert.NoError(t, UnmarshalJsonMap(map[string]any{}, &c)) { + assert.Equal(t, Env(2), c.Env) + } + }) + + t.Run("by ptr", func(t *testing.T) { + type ( + Env int + Config struct { + Env *Env `json:",env=INT_ENV,default=0"` + } + ) + + var c Config + if assert.NoError(t, UnmarshalJsonMap(map[string]any{}, &c)) { + assert.Equal(t, Env(2), *c.Env) + } + }) +} + func BenchmarkDefaultValue(b *testing.B) { for i := 0; i < b.N; i++ { var a struct { diff --git a/core/mapping/utils.go b/core/mapping/utils.go index b59fde7f1..3e08095ad 100644 --- a/core/mapping/utils.go +++ b/core/mapping/utils.go @@ -583,6 +583,10 @@ func toFloat64(v any) (float64, bool) { } } +func toReflectValue(tp reflect.Type, v any) reflect.Value { + return reflect.ValueOf(v).Convert(Deref(tp)) +} + func usingDifferentKeys(key string, field reflect.StructField) bool { if len(field.Tag) > 0 { if _, ok := field.Tag.Lookup(key); !ok {