From a1058255371885e5f4b1c754ce4b242d3ddb7d6e Mon Sep 17 00:00:00 2001 From: jose antonio hernandez hernandez Date: Thu, 2 Apr 2026 22:40:54 -0600 Subject: [PATCH 1/2] fix(custom-fields): preserve JSON objects/arrays in --custom values Problem Custom fields provided via --custom were always treated as plain strings in key paths that needed structured payloads. This broke User Picker fields (single and multi) and other custom fields requiring nested objects because Jira expected a JSON object/array, not an escaped string. Motivation Users editing issues with commands like the one below would hit Jira validation errors even when passing valid JSON. Example command (values anonymized) jira issue edit DEMOSERVICES-16216 --custom 'code-reviewer={"name":"redacted.user@example.com"}' --no-input Before this fix (internal payload sent) "customfield_xxxxx": [{"set": "{\"name\":\"redacted.user@example.com\"}"}] After this fix (internal payload sent) "customfield_xxxxx": [{"set": {"name": "redacted.user@example.com"}}] What changed - Added a dedicated parser in pkg/jira/customfield_parser.go that detects JSON containers (object/array) and unmarshals them while preserving existing behavior for non-container primitives/strings. - Wired create flow to pass parsed object/array directly into fields payload (instead of forcing string). - Wired edit flow to emit set operations with any typed value (object/array) so Jira receives correct structured JSON. - Kept pkg/jira/customfield.go focused on type declarations; parser logic is now isolated in its own file. Testing - Added unit tests in pkg/jira/customfield_json_test.go for create and edit with deeply nested JSON structures. - Verified with: go test ./pkg/jira --- pkg/jira/create.go | 4 ++ pkg/jira/customfield.go | 4 ++ pkg/jira/customfield_json_test.go | 80 +++++++++++++++++++++++++++++++ pkg/jira/customfield_parser.go | 24 ++++++++++ pkg/jira/edit.go | 4 ++ 5 files changed, 116 insertions(+) create mode 100644 pkg/jira/customfield_json_test.go create mode 100644 pkg/jira/customfield_parser.go diff --git a/pkg/jira/create.go b/pkg/jira/create.go index bec14918..cc6f5949 100644 --- a/pkg/jira/create.go +++ b/pkg/jira/create.go @@ -240,6 +240,10 @@ func constructCustomFields(fields map[string]string, configuredFields []IssueTyp if identifier != strings.ToLower(key) { continue } + if parsed, ok := parseCustomFieldJSONContainer(val); ok { + data.Fields.M.customFields[configured.Key] = parsed + continue + } switch configured.Schema.DataType { case customFieldFormatOption: diff --git a/pkg/jira/customfield.go b/pkg/jira/customfield.go index 92f3c675..935ca26c 100644 --- a/pkg/jira/customfield.go +++ b/pkg/jira/customfield.go @@ -19,6 +19,10 @@ type customFieldTypeStringSet struct { Set string `json:"set"` } +type customFieldTypeAnySet struct { + Set any `json:"set"` +} + type customFieldTypeOption struct { Value string `json:"value"` } diff --git a/pkg/jira/customfield_json_test.go b/pkg/jira/customfield_json_test.go new file mode 100644 index 00000000..43dd31e9 --- /dev/null +++ b/pkg/jira/customfield_json_test.go @@ -0,0 +1,80 @@ +package jira + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConstructCustomFieldsParsesNestedJSONObjectForCreate(t *testing.T) { + req := CreateRequest{ + Project: "TEST", + IssueType: "Task", + Summary: "Test issue", + CustomFields: map[string]string{ + "reviewers": `{"accountId":"abc","profile":{"team":{"id":"42"}}}`, + }, + } + req.WithCustomFields([]IssueTypeField{ + { + Name: "reviewers", + Key: "customfield_10042", + Schema: struct { + DataType string `json:"type"` + Items string `json:"items,omitempty"` + }{ + DataType: "user", + }, + }, + }) + + body, err := json.Marshal((&Client{}).getRequestData(&req)) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "update": {}, + "fields": { + "project": {"key": "TEST"}, + "issuetype": {"name": "Task"}, + "summary": "Test issue", + "customfield_10042": { + "accountId": "abc", + "profile": {"team": {"id": "42"}} + } + } + }`, string(body)) +} + +func TestConstructCustomFieldsParsesNestedJSONObjectForEdit(t *testing.T) { + req := EditRequest{ + CustomFields: map[string]string{ + "reviewers": `{"accountId":"abc","profile":{"team":{"members":[{"id":"1"},{"id":"2"}]}}}`, + }, + } + req.WithCustomFields([]IssueTypeField{ + { + Name: "reviewers", + Key: "customfield_10042", + Schema: struct { + DataType string `json:"type"` + Items string `json:"items,omitempty"` + }{ + DataType: "user", + }, + }, + }) + + body, err := json.Marshal(getRequestDataForEdit(&req)) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "update": { + "customfield_10042": [{ + "set": { + "accountId": "abc", + "profile": {"team": {"members": [{"id":"1"},{"id":"2"}]}} + } + }] + }, + "fields": {"parent": {}} + }`, string(body)) +} diff --git a/pkg/jira/customfield_parser.go b/pkg/jira/customfield_parser.go new file mode 100644 index 00000000..8ff015b3 --- /dev/null +++ b/pkg/jira/customfield_parser.go @@ -0,0 +1,24 @@ +package jira + +import ( + "encoding/json" + "strings" +) + +func parseCustomFieldJSONContainer(v string) (any, bool) { + trimmed := strings.TrimSpace(v) + if trimmed == "" { + return nil, false + } + + if !(strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[")) { + return nil, false + } + + var out any + if err := json.Unmarshal([]byte(trimmed), &out); err != nil { + return nil, false + } + + return out, true +} diff --git a/pkg/jira/edit.go b/pkg/jira/edit.go index a8e4bcd2..f42b4ea9 100644 --- a/pkg/jira/edit.go +++ b/pkg/jira/edit.go @@ -371,6 +371,10 @@ func constructCustomFieldsForEdit(fields map[string]string, configuredFields []I if identifier != strings.ToLower(key) { continue } + if parsed, ok := parseCustomFieldJSONContainer(val); ok { + data.Update.M.customFields[configured.Key] = []customFieldTypeAnySet{{Set: parsed}} + continue + } switch configured.Schema.DataType { case customFieldFormatOption: From c36b12f07fa06b8120343ffcc533b2cd4a0f631c Mon Sep 17 00:00:00 2001 From: jose antonio hernandez hernandez Date: Thu, 2 Apr 2026 22:51:19 -0600 Subject: [PATCH 2/2] chore(lint): satisfy staticcheck QF1001 in custom field parser Replace negated OR condition with equivalent De Morgan form to satisfy golangci-lint/staticcheck in CI. --- pkg/jira/customfield_parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/jira/customfield_parser.go b/pkg/jira/customfield_parser.go index 8ff015b3..3a349fa2 100644 --- a/pkg/jira/customfield_parser.go +++ b/pkg/jira/customfield_parser.go @@ -11,7 +11,7 @@ func parseCustomFieldJSONContainer(v string) (any, bool) { return nil, false } - if !(strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[")) { + if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") { return nil, false }