Skip to content

fix(sync): handle non-string YAML map keys in config parsing#1990

Open
anxkhn wants to merge 1 commit into
open-feature:mainfrom
anxkhn:patch-11
Open

fix(sync): handle non-string YAML map keys in config parsing#1990
anxkhn wants to merge 1 commit into
open-feature:mainfrom
anxkhn:patch-11

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 7, 2026

Copy link
Copy Markdown

What

A YAML flag config that contains a nested mapping with a non-string key (for
example an unquoted numeric key inside an object variant, or under metadata)
currently fails to load entirely, and the whole file is rejected with a cryptic
internal error:

json: unsupported type: map[interface {}]interface {}

This affects every YAML sync source (file, http, blob), all of which route
through utils.ConvertToJSON -> utils.YAMLToJSON.

Reproduction

A minimal, realistic flag definition that uses an object variant with numeric
keys is enough to trigger it:

flags:
  myObjectFlag:
    state: ENABLED
    defaultVariant: config
    variants:
      config:
        1: first
        2: second

Loading this config (via a file:, http:, or blob: sync) fails the entire
file rather than just that flag, with the map[interface {}]interface {} error
above.

Root cause

YAMLToJSON decodes the document into map[string]interface{} with
gopkg.in/yaml.v3 and then json.Marshals the result. yaml.v3 only forces
string keys for the top-level map (because the decode target type does).
Any nested mapping with a non-string key is decoded as
map[interface{}]interface{}, which encoding/json cannot marshal, so the
marshal step fails and the whole config is rejected.

Fix

Recursively convert nested map[interface{}]interface{} into
map[string]interface{} (stringifying the keys) before marshaling. yaml.v3 is
kept as the parser, so YAML 1.2 value semantics are unchanged:

  • unquoted on/off/yes/no/NO still parse as strings (no "Norway problem"),
  • timestamps keep their existing normalization,
  • duplicate keys and top-level scalar inputs still error exactly as before.

The change is purely additive: configs that already loaded are unaffected, and
configs with nested non-string keys now load instead of failing. No dependency
change.

Tests

Adds two table cases to core/pkg/utils/yaml_test.go:

  • nested non-string map keys - the object-variant config above now converts to
    JSON with stringified keys ("1", "2"); red before this change, green after.
  • unquoted boolean-like keys and values stay strings - pins YAML 1.2 value
    semantics so a future parser swap that reintroduced boolean coercion would be
    caught. Passes on main too.

go test -race -covermode=atomic -cover -short ./pkg/utils/... passes (97.2%
coverage); golangci-lint (v2.7.2) is clean; go mod tidy leaves go.mod/go.sum
unchanged.

YAMLToJSON unmarshaled YAML into map[string]interface{} with gopkg.in/yaml.v3
and then json.Marshal'd the result. yaml.v3 only forces string keys for the
top-level map; any nested mapping with a non-string key (e.g. an unquoted
numeric key in an object variant or under metadata) is decoded as
map[interface{}]interface{}, which encoding/json cannot marshal. The whole
config was then rejected with a cryptic internal error:
"json: unsupported type: map[interface {}]interface {}".

This is reachable from every YAML sync source (file, http, blob), all of
which route through ConvertToJSON -> YAMLToJSON.

Recursively convert nested map[interface{}]interface{} into
map[string]interface{} (stringifying the keys) before marshaling. yaml.v3 is
kept as the parser, so YAML 1.2 value semantics are unchanged: unquoted
on/off/yes/no/NO stay strings, timestamps keep their normalization, and
duplicate/top-level-scalar inputs still error exactly as before. The change is
purely additive: configs that already loaded are unaffected, and configs with
nested non-string keys now load instead of failing the entire file. No
dependency change.

Add regression tests: a nested numeric object-variant key now converts to
stringified JSON keys, and unquoted boolean-like keys and values still parse as
strings.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn
anxkhn requested review from a team as code owners July 7, 2026 11:33
@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for polite-licorice-3db33c canceled.

Name Link
🔨 Latest commit 2e7affa
🔍 Latest deploy log https://app.netlify.com/projects/polite-licorice-3db33c/deploys/6a4ce4034d7a7b0007f034fd

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The YAMLToJSON function now converts YAML-decoded map keys (e.g., map[interface{}]interface{}) into map[string]interface{} recursively before JSON marshaling, via a new convertMapKeys helper. Tests were extended to cover nested non-string keys and unquoted boolean-like values.

Changes

YAML Key Conversion Fix

Layer / File(s) Summary
Recursive key conversion before JSON marshaling
core/pkg/utils/yaml.go
Adds convertMapKeys to recursively convert non-string map keys (e.g., map[interface{}]interface{}) to strings and applies it before marshaling in YAMLToJSON.
Test coverage for key conversion
core/pkg/utils/yaml_test.go
Adds test cases for nested non-string map keys and unquoted boolean-like keys/values in TestYAMLToJSON.

Estimated code review effort: 2 (Simple) | ~10 minutes

Related Issues: None specified.

Related PRs: None specified.

Suggested labels: bug, core

Suggested reviewers: None specified.

Poem

A rabbit hopped through YAML's maze,
Where keys of odd types once did daze,
Now strings they turn, recursive and neat,
So JSON marshals without defeat,
Nested maps sing a tidier praise. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing YAML config parsing for nested non-string map keys.
Description check ✅ Passed The description is detailed and directly explains the YAML parsing bug, fix, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
core/pkg/utils/yaml.go (1)

29-56: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Solid fix; correctly targets yaml.v3's non-string-key mapping behavior.

The recursive convertMapKeys correctly handles the documented gopkg.in/yaml.v3 behavior where mappings containing non-string keys are decoded as map[interface{}]interface{} (as opposed to map[string]interface{} when all keys are strings), avoiding the json: unsupported type failure described in the PR.

One edge case worth being aware of: stringifying keys via fmt.Sprintf("%v", key) (line 45) can cause silent key collisions if a mapping mixes keys that stringify identically (e.g., an int key 1 alongside a string key "1" in the same YAML mapping) — one entry would silently overwrite the other rather than surfacing an error. This is unlikely in practice given typical config shapes, but if strict correctness matters here, consider detecting/rejecting collisions rather than silently overwriting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/pkg/utils/yaml.go` around lines 29 - 56, The recursive key conversion in
convertMapKeys currently stringifies non-string YAML mapping keys with
fmt.Sprintf, which can silently overwrite entries when different keys collapse
to the same string. Update convertMapKeys to detect and reject key collisions
(or otherwise surface an error) while preserving the existing recursive handling
of map[interface{}]interface{} and []interface{} values so yaml.v3 inputs are
converted safely for json.Marshal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/pkg/utils/yaml.go`:
- Around line 29-56: The recursive key conversion in convertMapKeys currently
stringifies non-string YAML mapping keys with fmt.Sprintf, which can silently
overwrite entries when different keys collapse to the same string. Update
convertMapKeys to detect and reject key collisions (or otherwise surface an
error) while preserving the existing recursive handling of
map[interface{}]interface{} and []interface{} values so yaml.v3 inputs are
converted safely for json.Marshal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ef4b3b3d-51eb-49d7-ac10-9bda236e8b04

📥 Commits

Reviewing files that changed from the base of the PR and between d99a3e3 and 2e7affa.

📒 Files selected for processing (2)
  • core/pkg/utils/yaml.go
  • core/pkg/utils/yaml_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant