Skip to content

feat(cmd): add dotnet appsettings.json export formats#243

Open
jevs242 wants to merge 2 commits into
Infisical:mainfrom
jevs242:feat/dotnet-appsettings-export
Open

feat(cmd): add dotnet appsettings.json export formats#243
jevs242 wants to merge 2 commits into
Infisical:mainfrom
jevs242:feat/dotnet-appsettings-export

Conversation

@jevs242
Copy link
Copy Markdown

@jevs242 jevs242 commented May 27, 2026

Summary

This PR adds support for a native hierarchical JSON configuration export format appsettings tailored for ASP.NET Core applications.

  • Why: ASP.NET Core uses deeply nested JSON objects for configuration settings rather than flat environment variables. Storing secrets as flat strings with double underscores (__) or colons (:) is standard for .NET but awkward to consume without conversion.
  • What: Adds the appsettings format, supporting recursive key-to-nested-object nesting using both __ and : delimiters, with robust JSON primitive type parsing (booleans, numbers, objects, arrays).

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR adds an appsettings export format for ASP.NET Core by recursively nesting flat secrets using __ and : as path delimiters and applying JSON type inference to values (booleans, numbers, arrays, objects).

  • formatAsAppSettingsJson splits keys on : (after normalising __:) and traverses/builds a nested map[string]interface{}, merging parsed JSON-object values with mergeMaps.
  • parseSecretValue runs json.Unmarshal on every value so strings that look like valid JSON primitives are emitted as typed JSON rather than quoted strings.
  • When two secrets produce conflicting key paths — one scalar and one as a nested-path prefix — the later-processed key silently overwrites the earlier one with no warning emitted to the user.

Confidence Score: 3/5

The new formatting logic works correctly for the tested happy paths, but a real scenario — having both a scalar secret and a nested-path secret sharing the same key prefix — silently drops one of the values with no warning to the user.

The conflict between a scalar key and a nested-path key that shares its prefix causes one secret value to be silently discarded. This is not a hypothetical edge case: any project storing both Logging and Logging__Level (a common ASP.NET pattern) would hit it. The issue is in the core new code path and has no test coverage for the failure mode.

packages/cmd/export.go — specifically the leaf-assignment branch of formatAsAppSettingsJson and its interaction with mergeMaps

Important Files Changed

Filename Overview
packages/cmd/export.go Adds appsettings format with __/: delimiter nesting and JSON type inference; has a silent data-loss issue when a scalar key and a nested-path key share the same prefix
packages/cmd/export_test.go Good coverage of happy-path nesting, merging, and type parsing; missing test for scalar-vs-nested-path key conflict

Reviews (1): Last reviewed commit: "refactor(cmd): remove redundant dotnet-j..." | Re-trigger Greptile

Comment thread packages/cmd/export.go
Comment on lines +396 to +411
for _, env := range envs {
val := parseSecretValue(env.Value)

normalizedKey := strings.ReplaceAll(env.Key, "__", ":")
parts := strings.Split(normalizedKey, ":")

current := result
for i, part := range parts {
if i == len(parts)-1 {
existingVal, exists := current[part]
existingMap, existingIsMap := existingVal.(map[string]interface{})
newMap, newIsMap := val.(map[string]interface{})
if exists && existingIsMap && newIsMap {
mergeMaps(existingMap, newMap)
} else {
current[part] = val
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Silent data loss on conflicting key paths

When two secrets produce conflicting key paths — one as a scalar value and another using that same key as a non-leaf prefix — one silently overwrites the other with no warning logged. Because util.SortSecretsByKeys runs before formatAsAppSettingsJson, AWS sorts before AWS__Region (or AWS:Region), so AWS=plain-string is processed first and set as a scalar, then AWS__Region=us-east-1 replaces AWS with a new nested map — discarding "plain-string". The reverse ordering (nested first, scalar second) hits the current[part] = val branch at line 411 and discards the map instead. Neither direction emits any warning, so users silently lose configuration values.

Comment thread packages/cmd/export.go
Comment on lines +360 to +371
func parseSecretValue(val string) interface{} {
trimmed := strings.TrimSpace(val)
var parsed interface{}
err := json.Unmarshal([]byte(trimmed), &parsed)
if err != nil {
return val
}
if parsed == nil && trimmed != "null" {
return val
}
return parsed
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The parseSecretValue function parses "null" correctly but the nil check is subtly redundant. json.Unmarshal sets parsed = nil only when the input is the JSON literal "null", so the guard trimmed != "null" can never fire — if unmarshal succeeded and parsed == nil, trimmed must equal "null". The double-negative condition adds no protection and can be removed.

Suggested change
func parseSecretValue(val string) interface{} {
trimmed := strings.TrimSpace(val)
var parsed interface{}
err := json.Unmarshal([]byte(trimmed), &parsed)
if err != nil {
return val
}
if parsed == nil && trimmed != "null" {
return val
}
return parsed
}
func parseSecretValue(val string) interface{} {
trimmed := strings.TrimSpace(val)
var parsed interface{}
err := json.Unmarshal([]byte(trimmed), &parsed)
if err != nil {
return val
}
// json.Unmarshal only sets parsed=nil when the input is the JSON literal "null"
return parsed
}

Comment on lines +84 to +115
input []models.SingleEnvironmentVariable
expected string
}{
{
name: "Empty input",
input: []models.SingleEnvironmentVariable{},
expected: "{}\n",
},
{
name: "Flat primitive types parsing",
input: []models.SingleEnvironmentVariable{
{Key: "PORT", Value: "8080"},
{Key: "DEBUG", Value: "true"},
{Key: "NAME", Value: "MyApplication"},
{Key: "NULL_VAL", Value: "null"},
},
expected: `{
"DEBUG": true,
"NAME": "MyApplication",
"NULL_VAL": null,
"PORT": 8080
}`,
},
{
name: "Nested keys with colons",
input: []models.SingleEnvironmentVariable{
{Key: "AWS:Region", Value: "us-east-1"},
{Key: "ConnectionStrings:Default", Value: "Server=myServerAddress;Database=myDataBase;"},
},
expected: `{
"AWS": {
"Region": "us-east-1"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Scalar-conflicts-with-nested-path case is untested

The test suite covers merging two maps and merging a JSON-object value with an explicit nested key, but there is no test for the scenario where a plain scalar key (AWS = "plain-string") conflicts with a dotted-path key that uses that prefix (AWS__Region = "us-east-1"). This is the scenario where silent data loss occurs. Adding a test case for this conflict in both orderings would surface the current silent-overwrite behavior and protect against regressions.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant