Skip to content

[release/13.2] Fix config boolean handling: crash on read, wrong types on write#15383

Merged
joperezr merged 1 commit intorelease/13.2from
fix/config-bool-crash
Mar 20, 2026
Merged

[release/13.2] Fix config boolean handling: crash on read, wrong types on write#15383
joperezr merged 1 commit intorelease/13.2from
fix/config-bool-crash

Conversation

@mitchdenny
Copy link
Member

@mitchdenny mitchdenny commented Mar 19, 2026

Description

Fix crash when reading boolean config values from aspire.config.json, and fix the write path to emit correct JSON types.

ConfigurationService.SetNestedValue wrote all values as JSON strings (e.g., "true" instead of true). When AspireConfigFile.Load() later deserialized the file, Dictionary<string, bool> could not parse the string values, causing a JsonException crash.

Four root causes fixed:

  1. Added FlexibleBooleanDictionaryConverter to AspireConfigFile.Features — tolerates both true and "true" on read, providing backward compatibility with config files written by older CLI versions.
  2. Fixed SetNestedValue to write proper JSON types — added ConvertToTypedJsonValue so booleans are written as true/false, integers as numbers, and only non-parseable values remain as strings.
  3. Fixed TryNormalizeSettingsFile to preserve JSON value types — uses JsonNode.DeepClone() instead of ToString() which corrupted booleans to strings when normalizing colon-separated keys.
  4. Normalized boolean casing in GetConfigurationAsyncIConfiguration converts JSON booleans true/false to .NET strings "True"/"False". The read path now normalizes to lowercase so aspire config get output matches JSON conventions and is consistent with what aspire config set writes.

Validation

  • 9 new tests added across AspireConfigFileTests, ConfigurationServiceTests, and ConfigurationHelperTests.
  • All tests in the affected test classes pass (0 failures).

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

Copilot AI review requested due to automatic review settings March 19, 2026 09:30
@github-actions
Copy link
Contributor

github-actions bot commented Mar 19, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15383

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15383"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a crash when reading boolean feature flags from aspire.config.json by (a) making AspireConfigFile.Features tolerant of legacy "true"/"false" string values on read, and (b) preserving JSON value types when normalizing colon-separated keys, plus updating config-writing behavior.

Changes:

  • Add FlexibleBooleanDictionaryConverter to AspireConfigFile.Features to support both JSON booleans and legacy string booleans.
  • Update settings normalization to preserve JSON types (e.g., booleans) when converting colon-separated root keys into nested objects.
  • Update ConfigurationService to write booleans (and integers) as JSON primitives instead of always writing strings; add tests covering these scenarios.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Aspire.Cli.Tests/Configuration/ConfigurationServiceTests.cs Adds tests verifying SetConfigurationAsync writes booleans/integers as JSON primitives and round-trips via AspireConfigFile.Load().
tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs Adds a test ensuring normalization preserves boolean JSON types and remains loadable.
tests/Aspire.Cli.Tests/Configuration/AspireConfigFileTests.cs Adds tests for loading features when values are booleans or legacy string booleans, and for save/load round-tripping.
src/Aspire.Cli/Utils/ConfigurationHelper.cs Preserves JSON node types during normalization via JsonNode.DeepClone() instead of ToString().
src/Aspire.Cli/Configuration/ConfigurationService.cs Writes typed JSON values via a new conversion helper instead of always writing strings.
src/Aspire.Cli/Configuration/AspireConfigFile.cs Applies FlexibleBooleanDictionaryConverter to Features for backward-compatible parsing.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +306 to +322
/// Converts a string value to its natural JSON type when possible.
/// Boolean strings ("true"/"false") become JSON booleans,
/// integer strings become JSON numbers, and everything else stays as a string.
/// </summary>
private static JsonNode? ConvertToTypedJsonValue(string value)
{
if (bool.TryParse(value, out var boolValue))
{
return JsonValue.Create(boolValue);
}

if (long.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var longValue))
{
return JsonValue.Create(longValue);
}

return value;
@mitchdenny mitchdenny changed the title Fix crash when reading boolean config values from aspire.config.json [release/13.2] Fix crash when reading boolean config values from aspire.config.json Mar 19, 2026
@dotnet-policy-service dotnet-policy-service bot added the needs-author-action An issue or pull request that requires more info or actions from the author. label Mar 19, 2026
Copy link
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

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

The core fixes are correct — FlexibleBooleanDictionaryConverter on Features, DeepClone() in TryNormalizeSettingsFile, and writing typed JSON values all address the root causes well, and the test coverage is solid. Two concerns flagged inline.

Comment on lines +348 to +365
{
// Convert dot notation to colon notation for IConfiguration access
var configKey = key.Replace('.', ':');
return Task.FromResult(configuration[configKey]);
var value = configuration[configKey];

// IConfiguration converts JSON booleans true/false to .NET strings "True"/"False".
// Normalize to lowercase so CLI output matches standard JSON conventions and is
// consistent with what aspire config set writes.
if (string.Equals(value, "True", StringComparison.Ordinal))
{
value = "true";
}
else if (string.Equals(value, "False", StringComparison.Ordinal))
{
value = "false";
}

return Task.FromResult(value);
Copy link
Member

Choose a reason for hiding this comment

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

The normalization here for GetConfigurationAsync (config get) is good. Note that FlattenJsonObject (used by GetAllConfigurationAsyncaspire config list) still calls kvp.Value.ToString() on leaf values and was not updated. For files written by the old code where booleans were stored as JSON strings ("true"), JsonNode.ToString() returns "\"true\"" (with quotes), while for proper JSON booleans it returns "true" (no quotes). So config get and config list can produce different representations for the same key on pre-fix files. Minor, but worth noting if you want the CLI output to be consistent.

@mitchdenny mitchdenny force-pushed the fix/config-bool-crash branch from 2b0a62f to 97e0fe3 Compare March 19, 2026 22:36
@mitchdenny
Copy link
Member Author

Review feedback addressed

All changes from the review have been incorporated in the force-pushed commit:

  1. Created FlexibleBooleanConverter : JsonConverter<bool> — registered globally via [JsonSourceGenerationOptions(Converters = [typeof(FlexibleBooleanConverter)])] on JsonSourceGenerationContext. Removed the dictionary converter attribute from AspireConfigFile.Features.

  2. Removed ConvertToTypedJsonValue entirelySetNestedValue always writes string values (no implicit type inference). This avoids the Norway problem where channel = "true" would be coerced to a JSON boolean.

  3. Removed GetConfigurationAsync normalization — since values are always written as strings, IConfiguration passes them through as-is. No Truetrue fixup needed.

  4. Kept DeepClone fix in TryNormalizeSettingsFile (unchanged).

  5. Updated tests — removed type-inference tests, added regression test for channel = "true" staying as JSON string, updated boolean round-trip test to verify FlexibleBooleanConverter handles string booleans.

@dotnet-policy-service dotnet-policy-service bot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Mar 19, 2026
@mitchdenny
Copy link
Member Author

@JamesNK Changed approach here based on your feedback. We'll just emit "true" ourselves - but we'll be forgiving on parsing.


// Value is written as a JSON string "true", not a JSON boolean true.
// The FlexibleBooleanConverter handles parsing "true" -> bool on read.
Assert.Contains("\"true\"", result);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of doing contains like this.

Instead, read result into JsonNode/JsonDocument, and get the value, and test its JSON type is String.

var result = File.ReadAllText(settingsFilePath);

// Must be a JSON string "true", not a JSON boolean true
Assert.Contains("\"true\"", result);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of doing contains like this.

Instead, read result into JsonNode/JsonDocument, and get the value, and test its JSON type is String.

var result = File.ReadAllText(settingsFilePath);

// Non-boolean/numeric values remain as strings
Assert.Contains("\"daily\"", result);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of doing contains like this.

Instead, read result into JsonNode/JsonDocument, and get the value, and test its JSON type is String.

Comment on lines +132 to +133
Assert.DoesNotContain("\"true\"", content);
Assert.DoesNotContain("\"false\"", content);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of doing contains like this.

Instead, read result into JsonNode/JsonDocument, and get the value, and test its JSON type is String.

@JamesNK JamesNK force-pushed the fix/config-bool-crash branch from 97e0fe3 to 2b0a62f Compare March 19, 2026 23:27
@JamesNK JamesNK changed the title [release/13.2] Fix crash when reading boolean config values from aspire.config.json [release/13.2] Fix config boolean handling: crash on read, wrong types on write Mar 19, 2026
@mitchdenny mitchdenny force-pushed the fix/config-bool-crash branch from 1a4f208 to f9989df Compare March 19, 2026 23:38
@mitchdenny mitchdenny requested a review from JamesNK March 20, 2026 00:06
When 'aspire config set features.X true' writes a JSON string "true" instead
of a JSON boolean, AspireConfigFile.Load() fails to deserialize the
Dictionary<string, bool> Features property.

Fix by adding a FlexibleBooleanConverter that tolerates both JSON booleans and
JSON string representations of booleans. The converter is registered globally
via JsonSourceGenerationOptions.Converters so all bool properties benefit.

Also fix TryNormalizeSettingsFile to use DeepClone() instead of ToString()
when normalizing colon-separated keys, which was corrupting non-string types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny force-pushed the fix/config-bool-crash branch from f9989df to fec66fa Compare March 20, 2026 00:32
@github-actions
Copy link
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings — 53 recordings uploaded (commit fec66fa)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23323666114

@joperezr joperezr merged commit aad1601 into release/13.2 Mar 20, 2026
506 of 509 checks passed
@joperezr joperezr deleted the fix/config-bool-crash branch March 20, 2026 02:57
@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Mar 20, 2026
joperezr added a commit to joperezr/aspire that referenced this pull request Mar 20, 2026
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.

4 participants