Repository: helm/helm (go)

Issue:
Helm `install --set-json` supports json arguments
## Proposal

The current implementation for `--set-json` requires that a key must be specified. In my mind, the ideal API for this flag should support user passing in a json string, which overrides `values.yaml` as one would do with `-f` flag.

```
# values.yaml
key1: 1
key2: 2
```

Usage now supports `helm install --set-json="{\"key1\": 1, \"key2\": 2}"`, in addition to `helm install --set-json=key1=1 --set-json=key2=2`.

Alternatively, we can also expose another flag `--from-json`, which supports this behavior. Let me know what you guys think, and I'm happy to take a stab at this if there's consensus!

## Context

For context on why this behavior is desirable, I'm using the helm binary programmatically to install helm charts with values based on in-memory objects. I would prefer not to dump the objects as yaml to file in order to use `-f` flag. Therefore, the alternative is to individually construct the argument for each key, value pair.

Pull request:
Supports json arguments
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:
This is a PR to support feature request from issue #13550 

- Add support for parsing full JSON objects with `--set-json`
- Update help text to clarify JSON value input methods
- Extend test coverage for JSON and value parsing scenarios
- Improve error handling for JSON parsing

**Special notes for your reviewer**:
closes #13550
I'm new to contributing helm and please let know if I did anything wrong. Thanks

**If applicable**:
- [x] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/ADOPTERS.md b/ADOPTERS.md
index a83519fea..e5ee1e7a2 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -5,21 +5,19 @@
 
 # Organizations Using Helm
 
-- [IBM](https://www.ibm.com)  
-- [InfoCert](https://www.infocert.it/)  
-- [Intercept](https://Intercept.cloud)  
-- [Microsoft](https://microsoft.com)  
-- [New Relic](https://www.newrelic.com)  
-- [Octopus Deploy](https://octopus.com/)  
-- [Omnistrate](https://omnistrate.com)  
-- [Oracle](www.oracle.com)  
-- [Percona](https://www.percona.com)  
-- [Qovery](https://www.qovery.com/)  
-- [Samsung SDS](https://www.samsungsds.com/)  
-- [Softonic](https://hello.softonic.com/)  
-- [SyncTune](https://mb-consulting.dev)  
-- [Syself](https://syself.com)  
-- [Ville de Montreal](https://montreal.ca)  
-
+- [IBM](https://www.ibm.com)
+- [InfoCert](https://www.infocert.it/)
+- [Microsoft](https://microsoft.com)
+- [Omnistrate](https://omnistrate.com)
+- [Octopus Deploy](https://octopus.com/)
+- [New Relic](https://www.newrelic.com)
+- [Qovery](https://www.qovery.com/)
+- [Samsung SDS](https://www.samsungsds.com/)
+- [Softonic](https://hello.softonic.com/)
+- [SyncTune](https://mb-consulting.dev)
+- [Syself](https://syself.com)
+- [Ville de Montreal](https://montreal.ca)
+- [Intercept](https://Intercept.cloud)
+- [Oracle](www.oracle.com)
 
 _This file is part of the CNCF official documentation for projects._
diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go
index 3d159babd..8d0f644d6 100644
--- a/cmd/helm/flags.go
+++ b/cmd/helm/flags.go
@@ -47,7 +47,7 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
 	f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
 	f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
 	f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
-	f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
+	f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2 or using json format: {\"key1\": jsonval1, \"key2\": \"jsonval2\"})")
 	f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
 }
 
diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index ec651140c..fe09dfc53 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -52,7 +52,7 @@ or use the '--set' flag and pass configuration from the command line, to force
 a string value use '--set-string'. You can use '--set-file' to set individual
 values from a file when the value itself is too long for the command line
 or is dynamically generated. You can also use '--set-json' to set json values
-(scalars/objects/arrays) from the command line.
+(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string.
 
     $ helm install -f myvalues.yaml myredis ./redis
 
@@ -72,6 +72,9 @@ or
 
     $ helm install --set-json 'master.sidecars=[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]' myredis ./redis
 
+or
+
+    $ helm install --set-json '{"master":{"sidecars":[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]}}' myredis ./redis
 
 You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
 last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index 7b4267894..6684f9ebf 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -53,7 +53,7 @@ or use the '--set' flag and pass configuration from the command line, to force s
 values, use '--set-string'. You can use '--set-file' to set individual
 values from a file when the value itself is too long for the command line
 or is dynamically generated. You can also use '--set-json' to set json values
-(scalars/objects/arrays) from the command line.
+(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string.
 
 You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
 last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go
index 24c47ecba..09af69f8b 100644
--- a/pkg/cli/values/options.go
+++ b/pkg/cli/values/options.go
@@ -17,6 +17,7 @@ limitations under the License.
 package values
 
 import (
+	"encoding/json"
 	"io"
 	"net/url"
 	"os"
@@ -62,8 +63,19 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
 
 	// User specified a value via --set-json
 	for _, value := range opts.JSONValues {
-		if err := strvals.ParseJSON(value, base); err != nil {
-			return nil, errors.Errorf("failed parsing --set-json data %s", value)
+		trimmedValue := strings.TrimSpace(value)
+		if len(trimmedValue) > 0 && trimmedValue[0] == '{' {
+			// If value is JSON object format, parse it as map
+			var jsonMap map[string]interface{}
+			if err := json.Unmarshal([]byte(trimmedValue), &jsonMap); err != nil {
+				return nil, errors.Errorf("failed parsing --set-json data JSON: %s", value)
+			}
+			base = mergeMaps(base, jsonMap)
+		} else {
+			// Otherwise, parse it as key=value format
+			if err := strvals.ParseJSON(value, base); err != nil {
+				return nil, errors.Errorf("failed parsing --set-json data %s", value)
+			}
 		}
 	}
 


Test patch:
diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go
index 9182e3cc8..5197a1b5e 100644
--- a/pkg/cli/values/options_test.go
+++ b/pkg/cli/values/options_test.go
@@ -23,7 +23,7 @@ import (
 	"helm.sh/helm/v4/pkg/getter"
 )
 
-func TestMergeValues(t *testing.T) {
+func TestMergeMaps(t *testing.T) {
 	nestedMap := map[string]interface{}{
 		"foo": "bar",
 		"baz": map[string]string{
@@ -86,3 +86,97 @@ func TestReadFile(t *testing.T) {
 		t.Errorf("Expected error when has special strings")
 	}
 }
+
+func TestMergeValues(t *testing.T) {
+	tests := []struct {
+		name     string
+		opts     Options
+		expected map[string]interface{}
+		wantErr  bool
+	}{
+		{
+			name: "set-json object",
+			opts: Options{
+				JSONValues: []string{`{"foo": {"bar": "baz"}}`},
+			},
+			expected: map[string]interface{}{
+				"foo": map[string]interface{}{
+					"bar": "baz",
+				},
+			},
+		},
+		{
+			name: "set-json key=value",
+			opts: Options{
+				JSONValues: []string{"foo.bar=[1,2,3]"},
+			},
+			expected: map[string]interface{}{
+				"foo": map[string]interface{}{
+					"bar": []interface{}{1.0, 2.0, 3.0},
+				},
+			},
+		},
+		{
+			name: "set regular value",
+			opts: Options{
+				Values: []string{"foo=bar"},
+			},
+			expected: map[string]interface{}{
+				"foo": "bar",
+			},
+		},
+		{
+			name: "set string value",
+			opts: Options{
+				StringValues: []string{"foo=123"},
+			},
+			expected: map[string]interface{}{
+				"foo": "123",
+			},
+		},
+		{
+			name: "set literal value",
+			opts: Options{
+				LiteralValues: []string{"foo=true"},
+			},
+			expected: map[string]interface{}{
+				"foo": "true",
+			},
+		},
+		{
+			name: "multiple options",
+			opts: Options{
+				Values:        []string{"a=foo"},
+				StringValues:  []string{"b=bar"},
+				JSONValues:    []string{`{"c": "foo1"}`},
+				LiteralValues: []string{"d=bar1"},
+			},
+			expected: map[string]interface{}{
+				"a": "foo",
+				"b": "bar",
+				"c": "foo1",
+				"d": "bar1",
+			},
+		},
+		{
+			name: "invalid json",
+			opts: Options{
+				JSONValues: []string{`{invalid`},
+			},
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.opts.MergeValues(getter.Providers{})
+			if (err != nil) != tt.wantErr {
+				t.Errorf("MergeValues() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !tt.wantErr && !reflect.DeepEqual(got, tt.expected) {
+				t.Errorf("MergeValues() = %v, want %v", got, tt.expected)
+			}
+		})
+	}
+}